diff --git a/.gitignore b/.gitignore index 08bf0bad6..944fb8418 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,7 @@ resources /omwlauncher /openmw /opencs +/niftest ## generated objects apps/openmw/config.hpp diff --git a/.travis.yml b/.travis.yml index e09fa46dc..1be8aa59c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,38 +1,42 @@ +os: + - linux + - osx language: cpp -compiler: - - gcc branches: only: - master + - coverity_scan - /openmw-.*$/ +env: + global: + # The next declaration is the encrypted COVERITY_SCAN_TOKEN, created + # via the "travis encrypt" command using the project repo's public key + - secure: "jybGzAdUbqt9vWR/GEnRd96BgAi/7Zd1+2HK68j/i/8+/1YH2XxLOy4Jv/DUBhBlJIkxs/Xv8dRcUlFOclZDHX1d/9Qnsqd3oUVkD7k1y7cTOWy9TBQaE/v/kZo3LpzA3xPwwthrb0BvqIbOfIELi5fS5s8ba85WFRg3AX70wWE=" +addons: + coverity_scan: + project: + name: "OpenMW/openmw" + description: "" + notification_email: scrawl@baseoftrash.de + build_command_prepend: "cmake ." + build_command: "make -j3" + branch_pattern: coverity_scan + before_install: - - pwd - - echo "yes" | sudo add-apt-repository "deb http://archive.ubuntu.com/ubuntu `lsb_release -sc` main universe restricted multiverse" - - echo "yes" | sudo apt-add-repository ppa:openmw/openmw - - sudo apt-get update -qq - - sudo apt-get install -qq libboost-all-dev libgtest-dev google-mock - - sudo apt-get install -qq libqt4-dev - - sudo apt-get install -qq libopenal-dev - - sudo apt-get install -qq libavcodec-dev libavformat-dev libavutil-dev libswscale-dev - - sudo apt-get install -qq libbullet-dev libogre-1.9-dev libmygui-dev libsdl2-dev libunshield-dev - - sudo mkdir /usr/src/gtest/build - - cd /usr/src/gtest/build - - sudo cmake .. -DBUILD_SHARED_LIBS=1 - - sudo make -j4 - - sudo ln -s /usr/src/gtest/build/libgtest.so /usr/lib/libgtest.so - - sudo ln -s /usr/src/gtest/build/libgtest_main.so /usr/lib/libgtest_main.so + - if [ "${TRAVIS_OS_NAME}" = "linux" ]; then ./CI/before_install.linux.sh; fi + - if [ "${TRAVIS_OS_NAME}" = "osx" ]; then ./CI/before_install.osx.sh; fi before_script: - - cd - - - mkdir build - - cd build - - cmake .. -DBUILD_WITH_CODE_COVERAGE=1 -DBUILD_UNITTESTS=1 -DBUILD_WITH_DPKG=1 + - if [ "${TRAVIS_OS_NAME}" = "linux" ]; then ./CI/before_script.linux.sh; fi + - if [ "${TRAVIS_OS_NAME}" = "osx" ]; then ./CI/before_script.osx.sh; fi script: - - make -j4 + - cd ./build + - if [ "$COVERITY_SCAN_BRANCH" != 1 ]; then make -j4; fi + - if [ "$COVERITY_SCAN_BRANCH" != 1 ] && [ "${TRAVIS_OS_NAME}" = "osx" ]; then make package; fi after_script: - - ./openmw_test_suite + - if [ "${TRAVIS_OS_NAME}" = "linux" ]; then ./openmw_test_suite; fi notifications: recipients: - - lgromanowski+travis.ci@gmail.com + - corrmage+travis-ci@gmail.com email: on_success: change on_failure: always diff --git a/AUTHORS.md b/AUTHORS.md new file mode 100644 index 000000000..90c543ada --- /dev/null +++ b/AUTHORS.md @@ -0,0 +1,207 @@ +Contributors +============ + +The OpenMW project was started in 2008 by Nicolay Korslund. +In the course of years many people have contributed to the project. + +If you feel your name is missing from this list, please notify a developer. + + +Programmers +----------- + + Marc Zinnschlag (Zini) - Lead Programmer/Project Manager + + Adam Hogan (aurix) + Aleksandar Jovanov + Alex Haddad (rainChu) + Alex McKibben (WeirdSexy) + Alexander Nadeau (wareya) + Alexander Olofsson (Ace) + Artem Kotsynyak (greye) + Arthur Moore (EmperorArthur) + athile + Bret Curtis (psi29a) + Britt Mathis (galdor557) + cc9cii + Chris Boyce (slothlife) + Chris Robinson (KittyCat) + Cory F. Cohen (cfcohen) + Cris Mihalache (Mirceam) + darkf + Dmitry Shkurskiy (endorph) + Douglas Diniz (Dgdiniz) + Douglas Mencken (dougmencken) + dreamer-dead + David Teviotdale (dteviot) + Edmondo Tommasina (edmondo) + Eduard Cot (trombonecot) + Eli2 + Emanuel Guével (potatoesmaster) + eroen + Evgeniy Mineev (sandstranger) + Fil Krynicki (filkry) + Gašper Sedej + gugus/gus + Hallfaer Tuilinn + Internecine + Jacob Essex (Yacoby) + Jannik Heller (scrawl) + Jason Hooks (jhooks) + jeaye + Jeffrey Haines (Jyby) + Jengerer + Joel Graff (graffy) + John Blomberg (fstp) + Jordan Ayers + Jordan Milne + Julien Voisin (jvoisin/ap0) + Karl-Felix Glatzer (k1ll) + Kevin Poitra (PuppyKevin) + Lars Söderberg (Lazaroth) + lazydev + Leon Saunders (emoose) + Lukasz Gromanowski (lgro) + Manuel Edelmann (vorenon) + Marc Bouvier (CramitDeFrog) + Marcin Hulist (Gohan) + Mark Siewert (mark76) + Marco Melletti (mellotanica) + Marco Schulze + Mateusz Kołaczek (PL_kolek) + megaton + Michael Hogan (Xethik) + Michael Mc Donnell + Michael Papageorgiou (werdanith) + Michał Bień (Glorf) + Miroslav Puda (pakanek) + MiroslavR + naclander + Narmo + Nathan Jeffords (blunted2night) + NeveHanter + Nikolay Kasyanov (corristo) + nobrakal + Nolan Poe (nopoe) + Paul McElroy (Greendogo) + Pieter van der Kloet (pvdk) + Radu-Marius Popovici (rpopovici) + rdimesio + riothamus + Robert MacGregor (Ragora) + Rohit Nirmal + Roman Melnik (Kromgart) + Roman Proskuryakov (humbug) + Sandy Carter (bwrsandman) + Scott Howard + Sebastian Wick (swick) + Sergey Shambir + sir_herrbatka + Stefan Galowicz (bogglez) + Stanislav Bobrov (Jiub) + Sylvain Thesnieres (Garvek) + terrorfisch + Thomas Luppi (Digmaster) + Tom Mason (wheybags) + Torben Leif Carrington (TorbenC) + viadanna + Vincent Heuken + vocollapse + +Packagers +--------- + + Alexander Olofsson (Ace) - Windows + Bret Curtis (psi29a) - Ubuntu Linux + Edmondo Tommasina (edmondo) - Gentoo Linux + Julian Ospald (hasufell) - Gentoo Linux + Karl-Felix Glatzer (k1ll) - Linux Binaries + Kenny Armstrong (artorius) - Fedora Linux + Nikolay Kasyanov (corristo) - Mac OS X + Sandy Carter (bwrsandman) - Arch Linux + +Public Relations and Translations +--------------------------------- + + Alex McKibben (WeirdSexy) - Podcaster + Artem Kotsynyak (greye) - Russian News Writer + Jim Clauwaert (Zedd) - Public Outreach + Julien Voisin (jvoisin/ap0) - French News Writer + Tom Koenderink (Okulo) - English News Writer + Lukasz Gromanowski (lgro) - English News Writer + Mickey Lyle (raevol) - Release Manager + Pithorn - Chinese News Writer + sir_herrbatka - Polish News Writer + Dawid Lakomy (Vedyimyn) - Polish News Writer + +Website +------- + + Lukasz Gromanowski (Lgro) - Website Administrator + Ryan Sardonic (Wry) - Wiki Editor + sir_herrbatka - Forum Administrator + +Formula Research +---------------- + + Hrnchamd + Epsilon + fragonard + Greendogo + HiPhish + modred11 + Myckel + natirips + Sadler + +Artwork +------- + + Necrod - OpenMW Logo + Mickey Lyle (raevol) - Wordpress Theme + Tom Koenderink (Okulo), SirHerrbatka, crysthala, Shnatsel - OpenMW Editor Icons + +Inactive Contributors +--------------------- + + Ardekantur + Armin Preiml + Berulacks + Carl Maxwell + Diggory Hardy + Dmitry Marakasov (AMDmi3) + ElderTroll + guidoj + Jan-Peter Nilsson (peppe) + Jan Borsodi + Josua Grawitter + juanmnzsk8 + Kingpix + Lordrea + Michal Sciubidlo + Nicolay Korslund + Nekochan + pchan3 + penguinroad + psi29a + sergoz + spyboot + Star-Demon + Thoronador + Yuri Krupenin + +Additional Credits +------------------ +In this section we would like to thank people not part of OpenMW for their work. + +Thanks to Maxim Nikolaev, +for allowing us to use his excellent Morrowind fan-art on our website and in other places. + +Thanks to DokterDume, +for kindly providing us with the Moon and Star logo, used as the application icon and project logo. + +Thanks to Kevin Ryan, +for creating the icon used for the Data Files tab of the OpenMW Launcher. + +Thanks to DejaVu team, +for their DejaVuLGCSansMono fontface, see DejaVu Font License.txt for their license terms. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..c2aa03dc2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,1622 @@ +0.35.0 +------ + + Bug #244: Clipping/static in relation to the ghostgate/fence sound. + Bug #531: Missing transparent menu items + Bug #811: Content Lists in openmw.cfg are overwritten + Bug #925: OpenCS doesn't launch because it thinks its already started + Bug #969: Water shader strange behaviour on AMD card + Bug #1049: Partially highlighted word in dialogue may cause incorrect line break + Bug #1069: omwlauncher.exe crashes due to file lock + Bug #1192: It is possible to jump on top of hostile creatures in combat + Bug #1342: Loud ambient sounds + Bug #1431: Creatures can climb the player + Bug #1605: Guard in CharGen doesn't turn around to face you when reaching stairs + Bug #1624: Moon edges don't transition properly + Bug #1634: Items dropped by PC have collision + Bug #1637: Weird NPC behaviour in Vivec, Hlaalu Ancestral Vaults? + Bug #1638: Cannot climb staircases + Bug #1648: Enchanted equipment badly handled at game reload + Bug #1663: Crash when casting spell at enemy near you + Bug #1683: Scale doesn't apply to animated collision nodes + Bug #1702: Active enchanted item forgotten + Bug #1730: Scripts names starting with digit(s) fail to compile + Bug #1743: Moons are transparent + Bug #1745: Shadows crash: Assertion `mEffects.empty()' failed. + Bug #1785: Can't equip two-handed weapon and shield + Bug #1809: Player falls too easily + Bug #1825: Sword of Perithia can´t run in OpenMW + Bug #1899: The launcher resets any alterations you´ve made in the mod list order, + Bug #1964: Idle voices/dialogs not triggered correctly + Bug #1980: Please, change default click behavior in OpenMW Launchers Data Files list + Bug #1984: Vampire corpses standing up when looting the first item + Bug #1985: Calm spell does nothing + Bug #1986: Spell name lights up on mouseover but spell cost does not + Bug #1989: Tooltip still shown when menu toggled off + Bug #2010: Raindrops Displayed While Underwater + Bug #2023: Walking into plants causes massive framedrop + Bug #2031: [MOD: Shrines - Restore Health and Cancel Options]: Restore health option doesn't work + Bug #2039: Lake Fjalding pillar of fire not rendered + Bug #2040: AI_follow should stop further from the target + Bug #2076: Slaughterfish AI + Bug #2077: Direction of long jump can be changed much more than it is possible in vanilla + Bug #2078: error during rendering: Object '' not found (const) + Bug #2105: Lockpicking causes screen sync glitch + Bug #2113: [MOD: Julan Ashlander Companion] Julan does not act correctly within the Ghostfence. + Bug #2123: Window glow mod: Collision issues + Bug #2133: Missing collision for bridges in Balmora when using Morrowind Rebirth 2.81 + Bug #2135: Casting a summon spell while the summon is active does not reset the summon. + Bug #2144: Changing equipment will unequip drawn arrows/bolts + Bug #2169: Yellow on faces when using opengl renderer and mods from overhaul on windows + Bug #2175: Pathgrid mods do not overwrite the existing pathgrid + Bug #2176: Morrowind -Russian localization end add-on ChaosHeart. Error in framelistener;object ;frenzying toush; not found + Bug #2181: Mod Morrowind crafting merchants die. + Bug #2182: mods changing skill progression double the bonus for class specialization + Bug #2183: Editor: Skills "use value" only allows integer between 0 and 99 + Bug #2184: Animated Morrowind Expanded produces an error on Open MW Launch + Bug #2185: Conditional Operator formats + Bug #2193: Quest: Gateway Ghost + Bug #2194: Cannot summon multiples of the same creature + Bug #2195: Pathgrid in the (0,0) exterior cell not loaded + Bug #2200: Outdoor NPCs can stray away and keep walking into a wall + Bug #2201: Creatures do not receive fall damage + Bug #2202: The enchantment the item can hold is calculated incorrectly + Bug #2203: Having the mod Living Cities of Vvardenfall running causes the game world to fail to load after leaving the prison ship + Bug #2204: Abot's Water Life - Book rendered incorrectly + Bug #2205: sound_waterfall script no longer compiles + Bug #2206: Dialogue script fails to compile (extra .) + Bug #2207: Script using – instead of - character does not compile + Bug #2208: Failing dialogue scripts in french Morrowind.esm + Bug #2214: LGNPC Vivec Redoran 1.62 and The King Rat (Size and inventory Issues) + Bug #2215: Beast races can use enchanted boots + Bug #2218: Incorrect names body parts in 3D models for open helmet with skinning + Bug #2219: Orcs in Ghorak Manor in Caldera don't attack if you pick their pockets. + Bug #2220: Chargen race preview head incorrect orientation + Bug #2223: Reseting rock falling animation + Bug #2224: Fortify Attribute effects do not stack when Spellmaking. + Bug #2226: OpenCS pseudo-crash + Bug #2230: segfaulting when entering Ald'ruhn with a specific mod: "fermeture la nuit" (closed by night) + Bug #2233: Area effect spells on touch do not have the area effect + Bug #2234: Dwarven Crossbow clips through the ground when dropped + Bug #2235: class SettingsBase<> reverses the order of entries with multiple keys. + Bug #2236: Weird two handed longsword + torch interaction + Bug #2237: Shooting arrows while sneaking do not agro + Bug #2238: Bipedal creatures not using weapons are not handled properly + Bug #2245: Incorrect topic highlighting in HT_SpyBaladas quest + Bug #2252: Tab completion incomplete for places using COC from the console. + Bug #2255: Camera reverts to first person on load + Bug #2259: enhancement: the save/load progress bar is not very progressive + Bug #2263: TogglePOV can not be bound to Alt key + Bug #2267: dialogue disabling via mod + Bug #2268: Highlighting Files with load order problems in Data Files tab of Launcher + Bug #2276: [Mod]ShotN issues with Karthwasten + Bug #2283: Count argument for PlaceAt functions not working + Bug #2284: Local map notes should be visible on door marker leading to the cell with the note + Bug #2293: There is a graphical glitch at the end of the spell's animation in 3rd Person (looking over the shoulder) view + Bug #2294: When using Skyrim UI Overhaul, the tops of pinnable menus are invisible + Bug #2302: Random leveled items repeat way too often in a single dungeon + Bug #2306: Enchanted arrows should not be retrievable from corpses + Bug #2308: No sound effect when drawing the next throwing knife + Bug #2309: Guards chase see the player character even if they're invisible + Bug #2319: Inverted controls and other issues after becoming a vampire + Bug #2324: Spells cast when crossing cell border are imprinted on the local map + Bug #2330: Actors with Drain Health effect retain health after dying + Bug #2331: tgm (god mode) won't allow the player to cast spells if the player doesn't have enough mana + Bug #2332: Error in framelistener: Need a skeleton to attach the arrow to + Feature #114: ess-Importer + Feature #504: Editor: Delete selected rows from result windows + Feature #1024: Addition of remaining equipping hotkeys + Feature #1067: Handle NIF interpolation type 4 (XYZ_ROTATION_KEY) + Feature #1125: AI fast-forward + Feature #1228: Drowning while knocked out + Feature #1325: Editor: Opening window and User Settings window cleanup + Feature #1537: Ability to change the grid size from 3x3 to 5x5 (or more with good pc) + Feature #1546: Leveled list script functions + Feature #1659: Test dialogue scripts in --script-all + Feature #1720: NPC lookAt controller + Feature #2178: Load initial particle system state from NIF files + Feature #2197: Editor: When clicking on a script error in the report window set cursor in script editor to the respective line/column + Feature #2261: Warn when loading save games with mod mismatch + Feature #2313: ess-Importer: convert global map exploration overlay + Feature #2318: Add commandline option to load a save game + Task #810: Rename "profile" to "content list" + Task #2196: Label local/global openmw.cfg files via comments + +0.34.0 +------ + + Bug #904: omwlauncher doesn't allow installing Tribunal and Bloodmoon if only MW is installed + Bug #986: Launcher: renaming profile names is broken + Bug #1061: "Browse to CD..." launcher crash + Bug #1135: Launcher crashes if user does not have write permission + Bug #1231: Current installer in launcher does not correctly import russian Morrowind.ini settings from setup.inx + Bug #1288: Fix the Alignment of the Resolution Combobox + Bug #1343: BIK videos occasionally out of sync with audio + Bug #1684: Morrowind Grass Mod graphical glitches + Bug #1734: NPC in fight with invisible/sneaking player + Bug #1982: Long class names are cut off in the UI + Bug #2012: Editor: OpenCS script compiler sometimes fails to find IDs + Bug #2015: Running while levitating does not affect speed but still drains fatigue + Bug #2018: OpenMW don´t reset modified cells to vanilla when a plugin is deselected and don´t apply changes to cells already visited. + Bug #2045: ToggleMenus command should close dialogue windows + Bug #2046: Crash: light_de_streetlight_01_223 + Bug #2047: Buglamp tooltip minor correction + Bug #2050: Roobrush floating texture bits + Bug #2053: Slaves react negatively to PC picking up slave's bracers + Bug #2055: Dremora corpses use the wrong model + Bug #2056: Mansilamat Vabdas's corpse is floating in the water + Bug #2057: "Quest: Larius Varro Tells A Little Story": Bounty not completely removed after finishing quest + Bug #2059: Silenced enemies try to cast spells anyway + Bug #2060: Editor: Special case implementation for top level window with single sub-window should be optional + Bug #2061: Editor: SubView closing that is not directly triggered by the user isn't handled properly + Bug #2063: Tribunal: Quest 'The Warlords' doesn't work + Bug #2064: Sneak attack on hostiles causes bounty + Bug #2065: Editor: Qt signal-slot error when closing a dialogue subview + Bug #2070: Loading ESP in OpenMW works but fails in OpenCS + Bug #2071: CTD in 0.33 + Bug #2073: Storm atronach animation stops now and then + Bug #2075: Molag Amur Region, Map shows water on solid ground + Bug #2080: game won't work with fair magicka regen + Bug #2082: NPCs appear frozen or switched off after leaving and quickly reentering a cell + Bug #2088: OpenMW is unable to play OGG files. + Bug #2093: Darth Gares talks to you in Ilunibi even when he's not there, screwing up the Main Quests + Bug #2095: Coordinate and rotation editing in the Reference table does not work. + Bug #2096: Some overflow fun and bartering exploit + Bug #2098: [D3D] Game crash on maximize + Bug #2099: Activate, player seems not to work + Bug #2104: Only labels are sensitive in buttons + Bug #2107: "Slowfall" effect is too weak + Bug #2114: OpenCS doesn't load an ESP file full of errors even though Vanilla MW Construction Set can + Bug #2117: Crash when encountering bandits on opposite side of river from the egg mine south of Balmora + Bug #2124: [Mod: Baldurians Transparent Glass Amor] Armor above head + Bug #2125: Unnamed NiNodes in weapons problem in First Person + Bug #2126: Dirty dialog script in tribunal.esm causing bug in Tribunal MQ + Bug #2128: Crash when picking character's face + Bug #2129: Disable the third-person zoom feature by default + Bug #2130: Ash storm particles shown too long during transition to clear sky + Bug #2137: Editor: exception caused by following the Creature column of a SoundGen record + Bug #2139: Mouse movement should be ignored during intro video + Bug #2143: Editor: Saving is broken + Bug #2145: OpenMW - crash while exiting x64 debug build + Bug #2152: You can attack Almalexia during her final monologue + Bug #2154: Visual effects behave weirdly after loading/taking a screenshot + Bug #2155: Vivec has too little magicka + Bug #2156: Azura's spirit fades away too fast + Bug #2158: [Mod]Julan Ashlander Companion 2.0: Negative magicka + Bug #2161: Editor: combat/magic/stealth values of creature not displayed correctly + Bug #2163: OpenMW can't detect death if the NPC die by the post damage effect of a magic weapon. + Bug #2168: Westly's Master Head Pack X – Some hairs aren't rendered correctly. + Bug #2170: Mods using conversations to update PC inconsistant + Bug #2180: Editor: Verifier doesn't handle Windows-specific path issues when dealing with resources + Bug #2212: Crash or unexpected behavior while closing OpenCS cell render window on OS X + Feature #238: Add UI to run INI-importer from the launcher + Feature #854: Editor: Add user setting to show status bar + Feature #987: Launcher: first launch instructions for CD need to be more explicit + Feature #1232: There is no way to set the "encoding" option using launcher UI. + Feature #1281: Editor: Render cell markers + Feature #1918: Editor: Functionality for Double-Clicking in Tables + Feature #1966: Editor: User Settings dialogue grouping/labelling/tooltips + Feature #2097: Editor: Edit position of references in 3D scene + Feature #2121: Editor: Add edit mode button to scene toolbar + Task #1965: Editor: Improve layout of user settings dialogue + +0.33.1 +------ + + Bug #2108: OpenCS fails to build + +0.33.0 +------ + + Bug #371: If console assigned to ` (probably to any symbolic key), "`" symbol will be added to console every time it closed + Bug #1148: Some books'/scrolls' contents are displayed incorrectly + Bug #1290: Editor: status bar is not updated when record filter is changed + Bug #1292: Editor: Documents are not removed on closing the last view + Bug #1301: Editor: File->Exit only checks the document it was issued from. + Bug #1353: Bluetooth on with no speaker connected results in significantly longer initial load times + Bug #1436: NPCs react from too far distance + Bug #1472: PC is placed on top of following NPC when changing cell + Bug #1487: Tall PC can get stuck in staircases + Bug #1565: Editor: Subviews are deleted on shutdown instead when they are closed + Bug #1623: Door marker on Ghorak Manor's balcony makes PC stuck + Bug #1633: Loaddoor to Sadrith Mora, Telvanni Council House spawns PC in the air + Bug #1655: Use Appropriate Application Icons on Windows + Bug #1679: Tribunal expansion, Meryn Othralas the backstage manager in the theatre group in Mournhold in the great bazaar district is floating a good feet above the ground. + Bug #1705: Rain is broken in third person + Bug #1706: Thunder and lighting still occurs while the game is paused during the rain + Bug #1708: No long jumping + Bug #1710: Editor: ReferenceableID drag to references record filter field creates incorrect filter + Bug #1712: Rest on Water + Bug #1715: "Cancel" button is not always on the same side of menu + Bug #1725: Editor: content file can be opened multiple times from the same dialogue + Bug #1730: [MOD: Less Generic Nerevarine] Compile failure attempting to enter the Corprusarium. + Bug #1733: Unhandled ffmpeg sample formats + Bug #1735: Editor: "Edit Record" context menu button not opening subview for journal infos + Bug #1750: Editor: record edits result in duplicate entries + Bug #1789: Editor: Some characters cannot be used in addon name + Bug #1803: Resizing the map does not keep the pre-resize center at the post-resize center + Bug #1821: Recovering Cloudcleaver quest: attacking Sosia is considered a crime when you side with Hlormar + Bug #1838: Editor: Preferences window appears off screen + Bug #1839: Editor: Record filter title should be moved two pixels to the right + Bug #1849: Subrecord error in MAO_Containers + Bug #1854: Knocked-out actors don't fully act knocked out + Bug #1855: "Soul trapped" sound doesn't play + Bug #1857: Missing sound effect for enchanted items with empty charge + Bug #1859: Missing console command: ResetActors (RA) + Bug #1861: Vendor category "MagicItems" is unhandled + Bug #1862: Launcher doesn't start if a file listed in launcher.cfg has correct name but wrong capitalization + Bug #1864: Editor: Region field for cell record in dialogue subview not working + Bug #1869: Editor: Change label "Musics" to "Music" + Bug #1870: Goblins killed while knocked down remain in knockdown-pose + Bug #1874: CellChanged events should not trigger when crossing exterior cell border + Bug #1877: Spriggans killed instantly if hit while regening + Bug #1878: Magic Menu text not un-highlighting correctly when going from spell to item as active magic + Bug #1881: Stuck in ceiling when entering castle karstaags tower + Bug #1884: Unlit torches still produce a burning sound + Bug #1885: Can type text in price field in barter window + Bug #1887: Equipped items do not emit sounds + Bug #1889: draugr lord aesliip will attack you and remain non-hostile + Bug #1892: Guard asks player to pay bounty of 0 gold + Bug #1895: getdistance should only return max float if ref and target are in different worldspaces + Bug #1896: Crash Report + Bug #1897: Conjured Equipment cant be re-equipped if removed + Bug #1898: Only Gidar Verothan follows you during establish the mine quest + Bug #1900: Black screen when you open the door and breath underwater + Bug #1904: Crash on casting recall spell + Bug #1906: Bound item checks should use the GMSTs + Bug #1907: Bugged door. Mournhold, The Winged Guar + Bug #1908: Crime reported for attacking Drathas Nerus's henchmen while they attack Dilborn + Bug #1909: Weird Quest Flow Infidelities quest + Bug #1910: Follower fighting with gone npc + Bug #1911: Npcs will drown themselves + Bug #1912: World map arrow stays static when inside a building + Bug #1920: Ulyne Henim disappears when game is loaded inside Vas + Bug #1922: alchemy-> potion of paralyze + Bug #1923: "levitation magic cannot be used here" shows outside of tribunal + Bug #1927: AI prefer melee over magic. + Bug #1929: Tamriel Rebuilt: Named cells that lie within the overlap with Morrowind.esm are not shown + Bug #1932: BTB - Spells 14.1 magic effects don´t overwrite the Vanilla ones but are added + Bug #1935: Stacks of items are worth more when sold individually + Bug #1940: Launcher does not list addon files if base game file is renamed to a different case + Bug #1946: Mod "Tel Nechim - moved" breaks savegames + Bug #1947: Buying/Selling price doesn't properly affect the growth of mercantile skill + Bug #1950: followers from east empire company quest will fight each other if combat happens with anything + Bug #1958: Journal can be scrolled indefinitely with a mouse wheel + Bug #1959: Follower not leaving party on quest end + Bug #1960: Key bindings not always saved correctly + Bug #1961: Spell merchants selling racial bonus spells + Bug #1967: segmentation fault on load saves + Bug #1968: Jump sounds are not controlled by footsteps slider, sound weird compared to footsteps + Bug #1970: PC suffers silently when taking damage from lava + Bug #1971: Dwarven Sceptre collision area is not removed after killing one + Bug #1974: Dalin/Daris Norvayne follows player indefinitely + Bug #1975: East Empire Company faction rank breaks during Raven Rock questline + Bug #1979: 0 strength = permanently over encumbered + Bug #1993: Shrine blessing in Maar Gan doesn't work + Bug #2008: Enchanted items do not recharge + Bug #2011: Editor: OpenCS script compiler doesn't handle member variable access properly + Bug #2016: Dagoth Ur already dead in Facility Cavern + Bug #2017: Fighters Guild Quest: The Code Book - dialogue loop when UMP is loaded. + Bug #2019: Animation of 'Correct UV Mudcrabs' broken + Bug #2022: Alchemy window - Removing ingredient doesn't remove the number of ingredients + Bug #2025: Missing mouse-over text for non affordable items + Bug #2028: [MOD: Tamriel Rebuilt] Crashing when trying to enter interior cell "Ruinous Keep, Great Hall" + Bug #2029: Ienith Brothers Thiev's Guild quest journal entry not adding + Feature #471: Editor: Special case implementation for top-level window with single sub-window + Feature #472: Editor: Sub-Window re-use settings + Feature #704: Font colors import from fallback settings + Feature #879: Editor: Open sub-views in a new top-level window + Feature #932: Editor: magic effect table + Feature #937: Editor: Path Grid table + Feature #938: Editor: Sound Gen table + Feature #1117: Death and LevelUp music + Feature #1226: Editor: Request UniversalId editing from table columns + Feature #1545: Targeting console on player + Feature #1597: Editor: Render terrain + Feature #1695: Editor: add column for CellRef's global variable + Feature #1696: Editor: use ESM::Cell's RefNum counter + Feature #1697: Redden player's vision when hit + Feature #1856: Spellcasting for non-biped creatures + Feature #1879: Editor: Run OpenMW with the currently edited content list + Task #1851: Move AI temporary state out of AI packages + Task #1865: Replace char type in records + +0.32.0 +------ + + Bug #1132: Unable to jump when facing a wall + Bug #1341: Summoned Creatures do not immediately disappear when killed. + Bug #1430: CharGen Revamped script does not compile + Bug #1451: NPCs shouldn't equip weapons prior to fighting + Bug #1461: Stopped start scripts do not restart on load + Bug #1473: Dead NPC standing and in 2 pieces + Bug #1482: Abilities are depleted when interrupted during casting + Bug #1503: Behaviour of NPCs facing the player + Bug #1506: Missing character, French edition: three-points + Bug #1528: Inventory very slow after 2 hours + Bug #1540: Extra arguments should be ignored for script functions + Bug #1541: Helseth's Champion: Tribunal + Bug #1570: Journal cannot be opened while in inventory screen + Bug #1573: PC joins factions at random + Bug #1576: NPCs aren't switching their weapons when out of ammo + Bug #1579: Guards detect creatures in far distance, instead on sight + Bug #1588: The Siege of the Skaal Village: bloodmoon + Bug #1593: The script compiler isn't recognising some names that contain a - + Bug #1606: Books: Question marks instead of quotation marks + Bug #1608: Dead bodies prevent door from opening/closing. + Bug #1609: Imperial guards in Sadrith Mora are not using their spears + Bug #1610: The bounty number is not displayed properly with high numbers + Bug #1620: Implement correct formula for auto-calculated NPC spells + Bug #1630: Boats standing vertically in Vivec + Bug #1635: Arrest dialogue is executed second time after I select "Go to jail" + Bug #1637: Weird NPC behaviour in Vivec, Hlaalu Ancestral Vaults? + Bug #1641: Persuasion dialog remains after loading, possibly resulting in crash + Bug #1644: "Goodbye" and similar options on dialogues prevents escape working properly. + Bug #1646: PC skill stats are not updated immediately when changing equipment + Bug #1652: Non-aggressive creature + Bug #1653: Quickloading while the container window is open crashes the game + Bug #1654: Priority of checks in organic containers + Bug #1656: Inventory items merge issue when repairing + Bug #1657: Attacked state of NPCs is not saved properly + Bug #1660: Rank dialogue condition ignored + Bug #1668: Game starts on day 2 instead of day 1 + Bug #1669: Critical Strikes while fighting a target who is currently fighting me + Bug #1672: OpenCS doesn't save the projects + Bug #1673: Fatigue decreasing by only one point when running + Bug #1675: Minimap and localmap graphic glitches + Bug #1676: Pressing the OK button on the travel menu cancels the travel and exits the menu + Bug #1677: Sleeping in a rented bed is considered a crime + Bug #1685: NPCs turn towards player even if invisible/sneaking + Bug #1686: UI bug: cursor is clicking "world/local" map button while inventory window is closed? + Bug #1690: Double clicking on a inventory window header doesn't close it. + Bug #1693: Spell Absorption does not absorb shrine blessings + Bug #1694: journal displays learned topics as quests + Bug #1700: Sideways scroll of text boxes + Bug #1701: Player enchanting requires player hold money, always 100% sucessful. + Bug #1704: self-made Fortify Intelligence/Drain willpower potions are broken + Bug #1707: Pausing the game through the esc menu will silence rain, pausing it by opening the inventory will not. + Bug #1709: Remesa Othril is hostile to Hlaalu members + Bug #1713: Crash on load after death + Bug #1719: Blind effect has slight border at the edge of the screen where it is ineffective. + Bug #1722: Crash after creating enchanted item, reloading saved game + Bug #1723: Content refs that are stacked share the same index after unstacking + Bug #1726: Can't finish Aengoth the Jeweler's quest : Retrieve the Scrap Metal + Bug #1727: Targets almost always resist soultrap scrolls + Bug #1728: Casting a soultrap spell on invalid target yields no message + Bug #1729: Chop attack doesn't work if walking diagonally + Bug #1732: Error handling for missing script function arguments produces weird message + Bug #1736: Alt-tabbing removes detail from overworld map. + Bug #1737: Going through doors with (high magnitude?) leviation will put the player high up, possibly even out of bounds. + Bug #1739: Setting a variable on an NPC from another NPC's dialogue result sets the wrong variable + Bug #1741: The wait dialogue doesn't black the screen out properly during waiting. + Bug #1742: ERROR: Object 'sDifficulty' not found (const) + Bug #1744: Night sky in Skies V.IV (& possibly v3) by SWG rendered incorrectly + Bug #1746: Bow/marksman weapon condition does not degrade with use + Bug #1749: Constant Battle Music + Bug #1752: Alt-Tabbing in the character menus makes the paper doll disappear temporarily + Bug #1753: Cost of training is not added to merchant's inventory + Bug #1755: Disposition changes do not persist if the conversation menu is closed by purchasing training. + Bug #1756: Caught Blight after being cured of Corprus + Bug #1758: Crash Upon Loading New Cell + Bug #1760: Player's Magicka is not recalculated upon drained or boosted intelligence + Bug #1761: Equiped torches lost on reload + Bug #1762: Your spell did not get a target. Soul trap. Gorenea Andrano + Bug #1763: Custom Spell Magicka Cost + Bug #1765: Azuras Star breaks on recharging item + Bug #1767: GetPCRank did not handle ignored explicit references + Bug #1772: Dark Brotherhood Assassins never use their Carved Ebony Dart, sticking to their melee weapon. + Bug #1774: String table overflow also occurs when loading TheGloryRoad.esm + Bug #1776: dagoth uthol runs in slow motion + Bug #1778: Incorrect values in spellmaking window + Bug #1779: Icon of Master Propylon Index is not visible + Bug #1783: Invisible NPC after looting corpse + Bug #1787: Health Calculation + Bug #1788: Skeletons, ghosts etc block doors when we try to open + Bug #1791: [MOD: LGNPC Foreign Quarter] NPC in completely the wrong place. + Bug #1792: Potions should show more effects + Bug #1793: Encumbrance while bartering + Bug #1794: Fortify attribute not affecting fatigue + Bug #1795: Too much magicka + Bug #1796: "Off by default" torch burning + Bug #1797: Fish too slow + Bug #1798: Rest until healed shouldn't show with full health and magicka + Bug #1802: Mark location moved + Bug #1804: stutter with recent builds + Bug #1810: attack gothens dremora doesnt agro the others. + Bug #1811: Regression: Crash Upon Loading New Cell + Bug #1812: Mod: "QuickChar" weird button placement + Bug #1815: Keys show value and weight, Vanilla Morrowind's keys dont. + Bug #1817: Persuasion results do not show using unpatched MW ESM + Bug #1818: Quest B3_ZainabBride moves to stage 47 upon loading save while Falura Llervu is following + Bug #1823: AI response to theft incorrect - only guards react, in vanilla everyone does. + Bug #1829: On-Target Spells Rendered Behind Water Surface Effects + Bug #1830: Galsa Gindu's house is on fire + Bug #1832: Fatal Error: OGRE Exception(2:InvalidParametersException) + Bug #1836: Attacked Guards open "fine/jail/resist"-dialogue after killing you + Bug #1840: Infinite recursion in ActionTeleport + Bug #1843: Escorted people change into player's cell after completion of escort stage + Bug #1845: Typing 'j' into 'Name' fields opens the journal + Bug #1846: Text pasted into the console still appears twice (Windows) + Bug #1847: "setfatigue 0" doesn't render NPC unconscious + Bug #1848: I can talk to unconscious actors + Bug #1866: Crash when player gets killed by a creature summoned by him + Bug #1868: Memory leaking when openmw window is minimized + Feature #47: Magic Effects + Feature #642: Control NPC mouth movement using current Say sound + Feature #939: Editor: Resources tables + Feature #961: AI Combat for magic (spells, potions and enchanted items) + Feature #1111: Collision script instructions (used e.g. by Lava) + Feature #1120: Command creature/humanoid magic effects + Feature #1121: Elemental shield magic effects + Feature #1122: Light magic effect + Feature #1139: AI: Friendly hits + Feature #1141: AI: combat party + Feature #1326: Editor: Add tooltips to all graphical buttons + Feature #1489: Magic effect Get/Mod/Set functions + Feature #1505: Difficulty slider + Feature #1538: Targeted scripts + Feature #1571: Allow creating custom markers on the local map + Feature #1615: Determine local variables from compiled scripts instead of the values in the script record + Feature #1616: Editor: Body part record verifier + Feature #1651: Editor: Improved keyboard navigation for scene toolbar + Feature #1666: Script blacklisting + Feature #1711: Including the Git revision number from the command line "--version" switch. + Feature #1721: NPC eye blinking + Feature #1740: Scene toolbar buttons for selecting which type of elements are rendered + Feature #1790: Mouse wheel scrolling for the journal + Feature #1850: NiBSPArrayController + Task #768: On windows, settings folder should be "OpenMW", not "openmw" + Task #908: Share keyframe data + Task #1716: Remove defunct option for building without FFmpeg + +0.31.0 +------ + + Bug #245: Cloud direction and weather systems differ from Morrowind + Bug #275: Local Map does not always show objects that span multiple cells + Bug #538: Update CenterOnCell (COC) function behavior + Bug #618: Local and World Map Textures are sometimes Black + Bug #640: Water behaviour at night + Bug #668: OpenMW doesn't support non-latin paths on Windows + Bug #746: OpenMW doesn't check if the background music was already played + Bug #747: Door is stuck if cell is left before animation finishes + Bug #772: Disabled statics are visible on map + Bug #829: OpenMW uses up all available vram, when playing for extended time + Bug #869: Dead bodies don't collide with anything + Bug #894: Various character creation issues + Bug #897/#1369: opencs Segmentation Fault after "new" or "load" + Bug #899: Various jumping issues + Bug #952: Reflection effects are one frame delayed + Bug #993: Able to interact with world during Wait/Rest dialog + Bug #995: Dropped items can be placed inside the wall + Bug #1008: Corpses always face up upon reentering the cell + Bug #1035: Random colour patterns appearing in automap + Bug #1037: Footstep volume issues + Bug #1047: Creation of wrong links in dialogue window + Bug #1129: Summoned creature time life duration seems infinite + Bug #1134: Crimes can be committed against hostile NPCs + Bug #1136: Creature run speed formula is incorrect + Bug #1150: Weakness to Fire doesn't apply to Fire Damage in the same spell + Bug #1155: NPCs killing each other + Bug #1166: Bittercup script still does not work + Bug #1178: .bsa file names are case sensitive. + Bug #1179: Crash after trying to load game after being killed + Bug #1180: Changing footstep sound location + Bug #1196: Jumping not disabled when showing messageboxes + Bug #1202: "strange" keys are not shown in binding menu, and are not saved either, but works + Bug #1216: Broken dialog topics in russian Morrowind + Bug #1217: Container content changes based on the current position of the mouse + Bug #1234: Loading/saving issues with dynamic records + Bug #1277: Text pasted into the console appears twice + Bug #1284: Crash on New Game + Bug #1303: It's possible to skip the chargen + Bug #1304: Slaughterfish should not detect the player unless the player is in the water + Bug #1311: Editor: deleting Record Filter line does not reset the filter + Bug #1324: ERROR: ESM Error: String table overflow when loading Animated Morrowind.esp + Bug #1328: Editor: Bogus Filter created when dragging multiple records to filter bar of non-applicable table + Bug #1331: Walking/running sound persist after killing NPC`s that are walking/running. + Bug #1334: Previously equipped items not shown as unequipped after attempting to sell them. + Bug #1335: Actors ignore vertical axis when deciding to attack + Bug #1338: Unknown toggle option for shadows + Bug #1339: "Ashlands Region" is visible when beginning new game during "Loading Area" process + Bug #1340: Guards prompt Player with punishment options after resisting arrest with another guard. + Bug #1348: Regression: Bug #1098 has returned with a vengeance + Bug #1349: [TR] TR_Data mesh tr_ex_imp_gatejamb01 cannot be activated + Bug #1352: Disabling an ESX file does not disable dependent ESX files + Bug #1355: CppCat Checks OpenMW + Bug #1356: Incorrect voice type filtering for sleep interrupts + Bug #1357: Restarting the game clears saves + Bug #1360: Seyda Neen silk rider dialog problem + Bug #1361: Some lights don't work + Bug #1364: It is difficult to bind "Mouse 1" to an action in the options menu + Bug #1370: Animation compilation mod does not work properly + Bug #1371: SL_Pick01.nif from third party fails to load in openmw, but works in Vanilla + Bug #1373: When stealing in front of Sellus Gravius cannot exit the dialog + Bug #1378: Installs to /usr/local are not working + Bug #1380: Loading a save file fail if one of the content files is disabled + Bug #1382: "getHExact() size mismatch" crash on loading official plugin "Siege at Firemoth.esp" + Bug #1386: Arkngthand door will not open + Bug #1388: Segfault when modifying View Distance in Menu options + Bug #1389: Crash when loading a save after dying + Bug #1390: Apostrophe characters not displayed [French version] + Bug #1391: Custom made icon background texture for magical weapons and stuff isn't scaled properly on GUI. + Bug #1393: Coin icon during the level up dialogue are off of the background + Bug #1394: Alt+F4 doesn't work on Win version + Bug #1395: Changing rings switches only the last one put on + Bug #1396: Pauldron parts aren't showing when the robe is equipped + Bug #1402: Dialogue of some shrines have wrong button orientation + Bug #1403: Items are floating in the air when they're dropped onto dead bodies. + Bug #1404: Forearms are not rendered on Argonian females + Bug #1407: Alchemy allows making potions from two of the same item + Bug #1408: "Max sale" button gives you all the items AND all the trader's gold + Bug #1409: Rest "Until Healed" broken for characters with stunted magicka. + Bug #1412: Empty travel window opens while playing through start game + Bug #1413: Save game ignores missing writing permission + Bug #1414: The Underground 2 ESM Error + Bug #1416: Not all splash screens in the Splash directory are used + Bug #1417: Loading saved game does not terminate + Bug #1419: Skyrim: Home of the Nords error + Bug #1422: ClearInfoActor + Bug #1423: ForceGreeting closes existing dialogue windows + Bug #1425: Cannot load save game + Bug #1426: Read skill books aren't stored in savegame + Bug #1427: Useless items can be set under hotkeys + Bug #1429: Text variables in journal + Bug #1432: When attacking friendly NPC, the crime is reported and bounty is raised after each swing + Bug #1435: Stealing priceless items is without punishment + Bug #1437: Door marker at Jobasha's Rare Books is spawning PC in the air + Bug #1440: Topic selection menu should be wider + Bug #1441: Dropping items on the rug makes them inaccessible + Bug #1442: When dropping and taking some looted items, bystanders consider that as a crime + Bug #1444: Arrows and bolts are not dropped where the cursor points + Bug #1445: Security trainers offering acrobatics instead + Bug #1447: Character dash not displayed, French edition + Bug #1448: When the player is killed by the guard while having a bounty on his head, the guard dialogue opens over and over instead of loading dialogue + Bug #1454: Script error in SkipTutorial + Bug #1456: Bad lighting when using certain Morrowind.ini generated by MGE + Bug #1457: Heart of Lorkan comes after you when attacking it + Bug #1458: Modified Keybindings are not remembered + Bug #1459: Dura Gra-Bol doesn't respond to PC attack + Bug #1462: Interior cells not loaded with Morrowind Patch active + Bug #1469: Item tooltip should show the base value, not real value + Bug #1477: Death count is not stored in savegame + Bug #1478: AiActivate does not trigger activate scripts + Bug #1481: Weapon not rendered when partially submerged in water + Bug #1483: Enemies are attacking even while dying + Bug #1486: ESM Error: Don't know what to do with INFO + Bug #1490: Arrows shot at PC can end up in inventory + Bug #1492: Monsters respawn on top of one another + Bug #1493: Dialogue box opens with follower NPC even if NPC is dead + Bug #1494: Paralysed cliffracers remain airbourne + Bug #1495: Dialogue box opens with follower NPC even the game is paused + Bug #1496: GUI messages are not cleared when loading another saved game + Bug #1499: Underwater sound sometimes plays when transitioning from interior. + Bug #1500: Targetted spells and water. + Bug #1502: Console error message on info refusal + Bug #1507: Bloodmoon MQ The Ritual of Beasts: Can't remove the arrow + Bug #1508: Bloodmoon: Fort Frostmoth, cant talk with Carnius Magius + Bug #1516: PositionCell doesn't move actors to current cell + Bug #1518: ForceGreeting broken for explicit references + Bug #1522: Crash after attempting to play non-music file + Bug #1523: World map empty after loading interior save + Bug #1524: Arrows in waiting/resting dialog act like minimum and maximum buttons + Bug #1525: Werewolf: Killed NPC's don't fill werewolfs hunger for blood + Bug #1527: Werewolf: Detect life detects wrong type of actor + Bug #1529: OpenMW crash during "the shrine of the dead" mission (tribunal) + Bug #1530: Selected text in the console has the same color as the background + Bug #1539: Barilzar's Mazed Band: Tribunal + Bug #1542: Looping taunts from NPC`s after death: Tribunal + Bug #1543: OpenCS crash when using drag&drop in script editor + Bug #1547: Bamz-Amschend: Centurion Archers combat problem + Bug #1548: The Missing Hand: Tribunal + Bug #1549: The Mad God: Tribunal, Dome of Serlyn + Bug #1557: A bounty is calculated from actual item cost + Bug #1562: Invisible terrain on top of Red Mountain + Bug #1564: Cave of the hidden music: Bloodmoon + Bug #1567: Editor: Deleting of referenceables does not work + Bug #1568: Picking up a stack of items and holding the enter key and moving your mouse around paints a bunch of garbage on screen. + Bug #1574: Solstheim: Drauger cant inflict damage on player + Bug #1578: Solstheim: Bonewolf running animation not working + Bug #1585: Particle effects on PC are stopped when paralyzed + Bug #1589: Tribunal: Crimson Plague quest does not update when Gedna Relvel is killed + Bug #1590: Failed to save game: compile error + Bug #1598: Segfault when making Drain/Fortify Skill spells + Bug #1599: Unable to switch to fullscreen + Bug #1613: Morrowind Rebirth duplicate objects / vanilla objects not removed + Bug #1618: Death notice fails to show up + Bug #1628: Alt+Tab Segfault + Feature #32: Periodic Cleanup/Refill + Feature #41: Precipitation and weather particles + Feature #568: Editor: Configuration setup + Feature #649: Editor: Threaded loading + Feature #930: Editor: Cell record saving + Feature #934: Editor: Body part table + Feature #935: Editor: Enchantment effect table + Feature #1162: Dialogue merging + Feature #1174: Saved Game: add missing creature state + Feature #1177: Saved Game: fog of war state + Feature #1312: Editor: Combat/Magic/Stealth values for creatures are not displayed + Feature #1314: Make NPCs and creatures fight each other + Feature #1315: Crime: Murder + Feature #1321: Sneak skill enhancements + Feature #1323: Handle restocking items + Feature #1332: Saved Game: levelled creatures + Feature #1347: modFactionReaction script instruction + Feature #1362: Animated main menu support + Feature #1433: Store walk/run toggle + Feature #1449: Use names instead of numbers for saved game files and folders + Feature #1453: Adding Delete button to the load menu + Feature #1460: Enable Journal screen while in dialogue + Feature #1480: Play Battle music when in combat + Feature #1501: Followers unable to fast travel with you + Feature #1520: Disposition and distance-based aggression/ShouldAttack + Feature #1595: Editor: Object rendering in cells + Task #940: Move license to locations where applicable + Task #1333: Remove cmake git tag reading + Task #1566: Editor: Object rendering refactoring + +0.30.0 +------ + + Bug #416: Extreme shaking can occur during cell transitions while moving + Bug #1003: Province Cyrodiil: Ogre Exception in Stirk + Bug #1071: Crash when given a non-existent content file + Bug #1080: OpenMW allows resting/using a bed while in combat + Bug #1097: Wrong punishment for stealing in Census and Excise Office at the start of a new game + Bug #1098: Unlocked evidence chests should get locked after new evidence is put into them + Bug #1099: NPCs that you attacked still fight you after you went to jail/paid your fine + Bug #1100: Taking items from a corpse is considered stealing + Bug #1126: Some creatures can't get close enough to attack + Bug #1144: Killed creatures seem to die again each time player transitions indoors/outdoors + Bug #1181: loading a saved game does not reset the player control status + Bug #1185: Collision issues in Addamasartus + Bug #1187: Athyn Sarethi mission, rescuing varvur sarethi from the doesnt end the mission + Bug #1189: Crash when entering interior cell "Gnisis, Arvs-Drelen" + Bug #1191: Picking up papers without inventory in new game + Bug #1195: NPCs do not equip torches in certain interiors + Bug #1197: mouse wheel makes things scroll too fast + Bug #1200: door blocked by monsters + Bug #1201: item's magical charges are only refreshed when they are used + Bug #1203: Scribs do not defend themselves + Bug #1204: creatures life is not empty when they are dead + Bug #1205: armor experience does not progress when hits are taken + Bug #1206: blood particules always red. Undeads and mechanicals should have a different one. + Bug #1209: Tarhiel never falls + Bug #1210: journal adding script is ran again after having saved/loaded + Bug #1224: Names of custom classes are not properly handled in save games + Bug #1227: Editor: Fixed case handling for broken localised versions of Morrowind.esm + Bug #1235: Indoors walk stutter + Bug #1236: Aborting intro movie brings up the menu + Bug #1239: NPCs get stuck when walking past each other + Bug #1240: BTB - Settings 14.1 and Health Bar. + Bug #1241: BTB - Character and Khajiit Prejudice + Bug #1248: GUI Weapon icon is changed to hand-to-hand after save load + Bug #1254: Guild ranks do not show in dialogue + Bug #1255: When opening a container and selecting "Take All", the screen flashes blue + Bug #1260: Level Up menu doesn't show image when using a custom class + Bug #1265: Quit Menu Has Misaligned Buttons + Bug #1270: Active weapon icon is not updated when weapon is repaired + Bug #1271: NPC Stuck in hovering "Jumping" animation + Bug #1272: Crash when attempting to load Big City esm file. + Bug #1276: Editor: Dropping a region into the filter of a cell subview fails + Bug #1286: Dialogue topic list clips with window frame + Bug #1291: Saved game: store faction membership + Bug #1293: Pluginless Khajiit Head Pack by ashiraniir makes OpenMW close. + Bug #1294: Pasting in console adds text to end, not at cursor + Bug #1295: Conversation loop when asking about "specific place" in Vivec + Bug #1296: Caius doesn't leave at start of quest "Mehra Milo and the Lost Prophecies" + Bug #1297: Saved game: map markers + Bug #1302: ring_keley script causes vector::_M_range_check exception + Bug #1309: Bug on "You violated the law" dialog + Bug #1319: Creatures sometimes rendered incorrectly + Feature #50: Ranged Combat + Feature #58: Sneaking Skill + Feature #73: Crime and Punishment + Feature #135: Editor: OGRE integration + Feature #541: Editor: Dialogue Sub-Views + Feature #853: Editor: Rework User Settings + Feature #944: Editor: lighting modes + Feature #945: Editor: Camera navigation mode + Feature #953: Trader gold + Feature #1140: AI: summoned creatures + Feature #1142: AI follow: Run stance + Feature #1154: Not all NPCs get aggressive when one is attacked + Feature #1169: Terrain threading + Feature #1172: Loading screen and progress bars during saved/loading game + Feature #1173: Saved Game: include weather state + Feature #1207: Class creation form does not remember + Feature #1220: Editor: Preview Subview + Feature #1223: Saved Game: Local Variables + Feature #1229: Quicksave, quickload, autosave + Feature #1230: Deleting saves + Feature #1233: Bribe gold is placed into NPCs inventory + Feature #1252: Saved Game: quick key bindings + Feature #1273: Editor: Region Map context menu + Feature #1274: Editor: Region Map drag & drop + Feature #1275: Editor: Scene subview drop + Feature #1282: Non-faction member crime recognition. + Feature #1289: NPCs return to default position + Task #941: Remove unused cmake files + +0.29.0 +------ + + Bug #556: Video soundtrack not played when music volume is set to zero + Bug #829: OpenMW uses up all available vram, when playing for extended time + Bug #848: Wrong amount of footsteps playing in 1st person + Bug #888: Ascended Sleepers have movement issues + Bug #892: Explicit references are allowed on all script functions + Bug #999: Graphic Herbalism (mod): sometimes doesn't activate properly + Bug #1009: Lake Fjalding AI related slowdown. + Bug #1041: Music playback issues on OS X >= 10.9 + Bug #1043: No message box when advancing skill "Speechcraft" while in dialog window + Bug #1060: Some message boxes are cut off at the bottom + Bug #1062: Bittercup script does not work ('end' variable) + Bug #1074: Inventory paperdoll obscures armour rating + Bug #1077: Message after killing an essential NPC disappears too fast + Bug #1078: "Clutterbane" shows empty charge bar + Bug #1083: UndoWerewolf fails + Bug #1088: Better Clothes Bloodmoon Plus 1.5 by Spirited Treasure pants are not rendered + Bug #1090: Start scripts fail when going to a non-predefined cell + Bug #1091: Crash: Assertion `!q.isNaN() && "Invalid orientation supplied as parameter"' failed. + Bug #1093: Weapons of aggressive NPCs are invisible after you exit and re-enter interior + Bug #1105: Magicka is depleted when using uncastable spells + Bug #1106: Creatures should be able to run + Bug #1107: TR cliffs have way too huge collision boxes in OpenMW + Bug #1109: Cleaning True Light and Darkness with Tes3cmd makes Addamasartus , Zenarbael and Yasamsi flooded. + Bug #1114: Bad output for desktop-file-validate on openmw.desktop (and opencs.desktop) + Bug #1115: Memory leak when spying on Fargoth + Bug #1137: Script execution fails (drenSlaveOwners script) + Bug #1143: Mehra Milo quest (vivec informants) is broken + Bug #1145: Issues with moving gold between inventory and containers + Bug #1146: Issues with picking up stacks of gold + Bug #1147: Dwemer Crossbows are held incorrectly + Bug #1158: Armor rating should always stay below inventory mannequin + Bug #1159: Quick keys can be set during character generation + Bug #1160: Crash on equip lockpick when + Bug #1167: Editor: Referenceables are not correctly loaded when dealing with more than one content file + Bug #1184: Game Save: overwriting an existing save does not actually overwrites the file + Feature #30: Loading/Saving (still missing a few parts) + Feature #101: AI Package: Activate + Feature #103: AI Package: Follow, FollowCell + Feature #138: Editor: Drag & Drop + Feature #428: Player death + Feature #505: Editor: Record Cloning + Feature #701: Levelled creatures + Feature #708: Improved Local Variable handling + Feature #709: Editor: Script verifier + Feature #764: Missing journal backend features + Feature #777: Creature weapons/shields + Feature #789: Editor: Referenceable record verifier + Feature #924: Load/Save GUI (still missing loading screen and progress bars) + Feature #946: Knockdown + Feature #947: Decrease fatigue when running, swimming and attacking + Feature #956: Melee Combat: Blocking + Feature #957: Area magic + Feature #960: Combat/AI combat for creatures + Feature #962: Combat-Related AI instructions + Feature #1075: Damage/Restore skill/attribute magic effects + Feature #1076: Soultrap magic effect + Feature #1081: Disease contraction + Feature #1086: Blood particles + Feature #1092: Interrupt resting + Feature #1101: Inventory equip scripts + Feature #1116: Version/Build number in Launcher window + Feature #1119: Resistance/weakness to normal weapons magic effect + Feature #1123: Slow Fall magic effect + Feature #1130: Auto-calculate spells + Feature #1164: Editor: Case-insensitive sorting in tables + +0.28.0 +------ + + Bug #399: Inventory changes are not visible immediately + Bug #417: Apply weather instantly when teleporting + Bug #566: Global Map position marker not updated for interior cells + Bug #712: Looting corpse delay + Bug #716: Problem with the "Vurt's Ascadian Isles Mod" mod + Bug #805: Two TR meshes appear black (v0.24RC) + Bug #841: Third-person activation distance taken from camera rather than head + Bug #845: NPCs hold torches during the day + Bug #855: Vvardenfell Visages Volume I some hairs don´t appear since 0,24 + Bug #856: Maormer race by Mac Kom - The heads are way up + Bug #864: Walk locks during loading in 3rd person + Bug #871: active weapon/magic item icon is not immediately made blank if item is removed during dialog + Bug #882: Hircine's Ring doesn't always work + Bug #909: [Tamriel Rebuilt] crashes in Akamora + Bug #922: Launcher writing merged openmw.cfg files + Bug #943: Random magnitude should be calculated per effect + Bug #948: Negative fatigue level should be allowed + Bug #949: Particles in world space + Bug #950: Hard crash on x64 Linux running --new-game (on startup) + Bug #951: setMagicka and setFatigue have no effect + Bug #954: Problem with equipping inventory items when using a keyboard shortcut + Bug #955: Issues with equipping torches + Bug #966: Shield is visible when casting spell + Bug #967: Game crashes when equipping silver candlestick + Bug #970: Segmentation fault when starting at Bal Isra + Bug #977: Pressing down key in console doesn't go forward in history + Bug #979: Tooltip disappears when changing inventory + Bug #980: Barter: item category is remembered, but not shown + Bug #981: Mod: replacing model has wrong position/orientation + Bug #982: Launcher: Addon unchecking is not saved + Bug #983: Fix controllers to affect objects attached to the base node + Bug #985: Player can talk to NPCs who are in combat + Bug #989: OpenMW crashes when trying to include mod with capital .ESP + Bug #991: Merchants equip items with harmful constant effect enchantments + Bug #994: Don't cap skills/attributes when set via console + Bug #998: Setting the max health should also set the current health + Bug #1005: Torches are visible when casting spells and during hand to hand combat. + Bug #1006: Many NPCs have 0 skill + Bug #1007: Console fills up with text + Bug #1013: Player randomly loses health or dies + Bug #1014: Persuasion window is not centered in maximized window + Bug #1015: Player status window scroll state resets on status change + Bug #1016: Notification window not big enough for all skill level ups + Bug #1020: Saved window positions are not rescaled appropriately on resolution change + Bug #1022: Messages stuck permanently on screen when they pile up + Bug #1023: Journals doesn't open + Bug #1026: Game loses track of torch usage. + Bug #1028: Crash on pickup of jug in Unexplored Shipwreck, Upper level + Bug #1029: Quick keys menu: Select compatible replacement when tool used up + Bug #1042: TES3 header data wrong encoding + Bug #1045: OS X: deployed OpenCS won't launch + Bug #1046: All damaged weaponry is worth 1 gold + Bug #1048: Links in "locked" dialogue are still clickable + Bug #1052: Using color codes when naming your character actually changes the name's color + Bug #1054: Spell effects not visible in front of water + Bug #1055: Power-Spell animation starts even though you already casted it that day + Bug #1059: Cure disease potion removes all effects from player, even your race bonus and race ability + Bug #1063: Crash upon checking out game start ship area in Seyda Neen + Bug #1064: openmw binaries link to unnecessary libraries + Bug #1065: Landing from a high place in water still causes fall damage + Bug #1072: Drawing weapon increases torch brightness + Bug #1073: Merchants sell stacks of gold + Feature #43: Visuals for Magic Effects + Feature #51: Ranged Magic + Feature #52: Touch Range Magic + Feature #53: Self Range Magic + Feature #54: Spell Casting + Feature #70: Vampirism + Feature #100: Combat AI + Feature #171: Implement NIF record NiFlipController + Feature #410: Window to restore enchanted item charge + Feature #647: Enchanted item glow + Feature #723: Invisibility/Chameleon magic effects + Feature #737: Resist Magicka magic effect + Feature #758: GetLOS + Feature #926: Editor: Info-Record tables + Feature #958: Material controllers + Feature #959: Terrain bump, specular, & parallax mapping + Feature #990: Request: unlock mouse when in any menu + Feature #1018: Do not allow view mode switching while performing an action + Feature #1027: Vertex morph animation (NiGeomMorpherController) + Feature #1031: Handle NiBillboardNode + Feature #1051: Implement NIF texture slot DarkTexture + Task #873: Unify OGRE initialisation + +0.27.0 +------ + + Bug #597: Assertion `dialogue->mId == id' failed in esmstore.cpp + Bug #794: incorrect display of decimal numbers + Bug #840: First-person sneaking camera height + Bug #887: Ambient sounds playing while paused + Bug #902: Problems with Polish character encoding + Bug #907: Entering third person using the mousewheel is possible even if it's impossible using the key + Bug #910: Some CDs not working correctly with Unshield installer + Bug #917: Quick character creation plugin does not work + Bug #918: Fatigue does not refill + Bug #919: The PC falls dead in Beshara - OpenMW nightly Win64 (708CDE2) + Feature #57: Acrobatics Skill + Feature #462: Editor: Start Dialogue + Feature #546: Modify ESX selector to handle new content file scheme + Feature #588: Editor: Adjust name/path of edited content files + Feature #644: Editor: Save + Feature #710: Editor: Configure script compiler context + Feature #790: God Mode + Feature #881: Editor: Allow only one instance of OpenCS + Feature #889: Editor: Record filtering + Feature #895: Extinguish torches + Feature #898: Breath meter enhancements + Feature #901: Editor: Default record filter + Feature #913: Merge --master and --plugin switches + +0.26.0 +------ + + Bug #274: Inconsistencies in the terrain + Bug #557: Already-dead NPCs do not equip clothing/items. + Bug #592: Window resizing + Bug #612: [Tamriel Rebuilt] Missing terrain (South of Tel Oren) + Bug #664: Heart of lorkhan acts like a dead body (container) + Bug #767: Wonky ramp physics & water + Bug #780: Swimming out of water + Bug #792: Wrong ground alignment on actors when no clipping + Bug #796: Opening and closing door sound issue + Bug #797: No clipping hinders opening and closing of doors + Bug #799: sliders in enchanting window + Bug #838: Pressing key during startup procedure freezes the game + Bug #839: Combat/magic stances during character creation + Bug #843: [Tribunal] Dark Brotherhood assassin appears without equipment + Bug #844: Resting "until healed" option given even with full stats + Bug #846: Equipped torches are invisible. + Bug #847: Incorrect formula for autocalculated NPC initial health + Bug #850: Shealt weapon sound plays when leaving magic-ready stance + Bug #852: Some boots do not produce footstep sounds + Bug #860: FPS bar misalignment + Bug #861: Unable to print screen + Bug #863: No sneaking and jumping at the same time + Bug #866: Empty variables in [Movies] section of Morrowind.ini gets imported into OpenMW.cfg as blank fallback option and crashes game on start. + Bug #867: Dancing girls in "Suran, Desele's House of Earthly Delights" don't dance. + Bug #868: Idle animations are repeated + Bug #874: Underwater swimming close to the ground is jerky + Bug #875: Animation problem while swimming on the surface and looking up + Bug #876: Always a starting upper case letter in the inventory + Bug #878: Active spell effects don't update the layout properly when ended + Bug #891: Cell 24,-12 (Tamriel Rebuilt) crashes on load + Bug #896: New game sound issue + Feature #49: Melee Combat + Feature #71: Lycanthropy + Feature #393: Initialise MWMechanics::AiSequence from ESM::AIPackageList + Feature #622: Multiple positions for inventory window + Feature #627: Drowning + Feature #786: Allow the 'Activate' key to close the countdialog window + Feature #798: Morrowind installation via Launcher (Linux/Max OS only) + Feature #851: First/Third person transitions with mouse wheel + Task #689: change PhysicActor::enableCollisions + Task #707: Reorganise Compiler + +0.25.0 +------ + + Bug #411: Launcher crash on OS X < 10.8 + Bug #604: Terrible performance drop in the Census and Excise Office. + Bug #676: Start Scripts fail to load + Bug #677: OpenMW does not accept script names with - + Bug #766: Extra space in front of topic links + Bug #793: AIWander Isn't Being Passed The Repeat Parameter + Bug #795: Sound playing with drawn weapon and crossing cell-border + Bug #800: can't select weapon for enchantment + Bug #801: Player can move while over-encumbered + Bug #802: Dead Keys not working + Bug #808: mouse capture + Bug #809: ini Importer does not work without an existing cfg file + Bug #812: Launcher will run OpenMW with no ESM or ESP selected + Bug #813: OpenMW defaults to Morrowind.ESM with no ESM or ESP selected + Bug #817: Dead NPCs and Creatures still have collision boxes + Bug #820: Incorrect sorting of answers (Dialogue) + Bug #826: mwinimport dumps core when given an unknown parameter + Bug #833: getting stuck in door + Bug #835: Journals/books not showing up properly. + Feature #38: SoundGen + Feature #105: AI Package: Wander + Feature #230: 64-bit compatibility for OS X + Feature #263: Hardware mouse cursors + Feature #449: Allow mouse outside of window while paused + Feature #736: First person animations + Feature #750: Using mouse wheel in third person mode + Feature #822: Autorepeat for slider buttons + +0.24.0 +------ + + Bug #284: Book's text misalignment + Bug #445: Camera able to get slightly below floor / terrain + Bug #582: Seam issue in Red Mountain + Bug #632: Journal Next Button shows white square + Bug #653: IndexedStore ignores index + Bug #694: Parser does not recognize float values starting with . + Bug #699: Resource handling broken with Ogre 1.9 trunk + Bug #718: components/esm/loadcell is using the mwworld subsystem + Bug #729: Levelled item list tries to add nonexistent item + Bug #730: Arrow buttons in the settings menu do not work. + Bug #732: Erroneous behavior when binding keys + Bug #733: Unclickable dialogue topic + Bug #734: Book empty line problem + Bug #738: OnDeath only works with implicit references + Bug #740: Script compiler fails on scripts with special names + Bug #742: Wait while no clipping + Bug #743: Problem with changeweather console command + Bug #744: No wait dialogue after starting a new game + Bug #748: Player is not able to unselect objects with the console + Bug #751: AddItem should only spawn a message box when called from dialogue + Bug #752: The enter button has several functions in trade and looting that is not impelemted. + Bug #753: Fargoth's Ring Quest Strange Behavior + Bug #755: Launcher writes duplicate lines into settings.cfg + Bug #759: Second quest in mages guild does not work + Bug #763: Enchantment cast cost is wrong + Bug #770: The "Take" and "Close" buttons in the scroll GUI are stretched incorrectly + Bug #773: AIWander Isn't Being Passed The Correct idle Values + Bug #778: The journal can be opened at the start of a new game + Bug #779: Divayth Fyr starts as dead + Bug #787: "Batch count" on detailed FPS counter gets cut-off + Bug #788: chargen scroll layout does not match vanilla + Feature #60: Atlethics Skill + Feature #65: Security Skill + Feature #74: Interaction with non-load-doors + Feature #98: Render Weapon and Shield + Feature #102: AI Package: Escort, EscortCell + Feature #182: Advanced Journal GUI + Feature #288: Trading enhancements + Feature #405: Integrate "new game" into the menu + Feature #537: Highlight dialogue topic links + Feature #658: Rotate, RotateWorld script instructions and local rotations + Feature #690: Animation Layering + Feature #722: Night Eye/Blind magic effects + Feature #735: Move, MoveWorld script instructions. + Feature #760: Non-removable corpses + +0.23.0 +------ + + Bug #522: Player collides with placeable items + Bug #553: Open/Close sounds played when accessing main menu w/ Journal Open + Bug #561: Tooltip word wrapping delay + Bug #578: Bribing works incorrectly + Bug #601: PositionCell fails on negative coordinates + Bug #606: Some NPCs hairs not rendered with Better Heads addon + Bug #609: Bad rendering of bone boots + Bug #613: Messagebox causing assert to fail + Bug #631: Segfault on shutdown + Bug #634: Exception when talking to Calvus Horatius in Mournhold, royal palace courtyard + Bug #635: Scale NPCs depending on race + Bug #643: Dialogue Race select function is inverted + Bug #646: Twohanded weapons don't work properly + Bug #654: Crash when dropping objects without a collision shape + Bug #655/656: Objects that were disabled or deleted (but not both) were added to the scene when re-entering a cell + Bug #660: "g" in "change" cut off in Race Menu + Bug #661: Arrille sells me the key to his upstairs room + Bug #662: Day counter starts at 2 instead of 1 + Bug #663: Cannot select "come unprepared" topic in dialog with Dagoth Ur + Bug #665: Pickpocket -> "Grab all" grabs all NPC inventory, even not listed in container window. + Bug #666: Looking up/down problem + Bug #667: Active effects border visible during loading + Bug #669: incorrect player position at new game start + Bug #670: race selection menu: sex, face and hair left button not totally clickable + Bug #671: new game: player is naked + Bug #674: buying or selling items doesn't change amount of gold + Bug #675: fatigue is not set to its maximum when starting a new game + Bug #678: Wrong rotation order causes RefData's rotation to be stored incorrectly + Bug #680: different gold coins in Tel Mara + Bug #682: Race menu ignores playable flag for some hairs and faces + Bug #685: Script compiler does not accept ":" after a function name + Bug #688: dispose corpse makes cross-hair to disappear + Bug #691: Auto equipping ignores equipment conditions + Bug #692: OpenMW doesnt load "loose file" texture packs that places resources directly in data folder + Bug #696: Draugr incorrect head offset + Bug #697: Sail transparency issue + Bug #700: "On the rocks" mod does not load its UV coordinates correctly. + Bug #702: Some race mods don't work + Bug #711: Crash during character creation + Bug #715: Growing Tauryon + Bug #725: Auto calculate stats + Bug #728: Failure to open container and talk dialogue + Bug #731: Crash with Mush-Mere's "background" topic + Feature #55/657: Item Repairing + Feature #62/87: Enchanting + Feature #99: Pathfinding + Feature #104: AI Package: Travel + Feature #129: Levelled items + Feature #204: Texture animations + Feature #239: Fallback-Settings + Feature #535: Console object selection improvements + Feature #629: Add levelup description in levelup layout dialog + Feature #630: Optional format subrecord in (tes3) header + Feature #641: Armor rating + Feature #645: OnDeath script function + Feature #683: Companion item UI + Feature #698: Basic Particles + Task #648: Split up components/esm/loadlocks + Task #695: mwgui cleanup + +0.22.0 +------ + + Bug #311: Potential infinite recursion in script compiler + Bug #355: Keyboard repeat rate (in Xorg) are left disabled after game exit. + Bug #382: Weird effect in 3rd person on water + Bug #387: Always use detailed shape for physics raycasts + Bug #420: Potion/ingredient effects do not stack + Bug #429: Parts of dwemer door not picked up correctly for activation/tooltips + Bug #434/Bug #605: Object movement between cells not properly implemented + Bug #502: Duplicate player collision model at origin + Bug #509: Dialogue topic list shifts inappropriately + Bug #513: Sliding stairs + Bug #515: Launcher does not support non-latin strings + Bug #525: Race selection preview camera wrong position + Bug #526: Attributes / skills should not go below zero + Bug #529: Class and Birthsign menus options should be preselected + Bug #530: Lock window button graphic missing + Bug #532: Missing map menu graphics + Bug #545: ESX selector does not list ESM files properly + Bug #547: Global variables of type short are read incorrectly + Bug #550: Invisible meshes collision and tooltip + Bug #551: Performance drop when loading multiple ESM files + Bug #552: Don't list CG in options if it is not available + Bug #555: Character creation windows "OK" button broken + Bug #558: Segmentation fault when Alt-tabbing with console opened + Bug #559: Dialog window should not be available before character creation is finished + Bug #560: Tooltip borders should be stretched + Bug #562: Sound should not be played when an object cannot be picked up + Bug #565: Water animation speed + timescale + Bug #572: Better Bodies' textures don't work + Bug #573: OpenMW doesn't load if TR_Mainland.esm is enabled (Tamriel Rebuilt mod) + Bug #574: Moving left/right should not cancel auto-run + Bug #575: Crash entering the Chamber of Song + Bug #576: Missing includes + Bug #577: Left Gloves Addon causes ESMReader exception + Bug #579: Unable to open container "Kvama Egg Sack" + Bug #581: Mimicking vanilla Morrowind water + Bug #583: Gender not recognized + Bug #586: Wrong char gen behaviour + Bug #587: "End" script statements with spaces don't work + Bug #589: Closing message boxes by pressing the activation key + Bug #590: Ugly Dagoth Ur rendering + Bug #591: Race selection issues + Bug #593: Persuasion response should be random + Bug #595: Footless guard + Bug #599: Waterfalls are invisible from a certain distance + Bug #600: Waterfalls rendered incorrectly, cut off by water + Bug #607: New beast bodies mod crashes + Bug #608: Crash in cell "Mournhold, Royal Palace" + Bug #611: OpenMW doesn't find some of textures used in Tamriel Rebuilt + Bug #613: Messagebox causing assert to fail + Bug #615: Meshes invisible from above water + Bug #617: Potion effects should be hidden until discovered + Bug #619: certain moss hanging from tree has rendering bug + Bug #621: Batching bloodmoon's trees + Bug #623: NiMaterialProperty alpha unhandled + Bug #628: Launcher in latest master crashes the game + Bug #633: Crash on startup: Better Heads + Bug #636: Incorrect Char Gen Menu Behavior + Feature #29: Allow ESPs and multiple ESMs + Feature #94: Finish class selection-dialogue + Feature #149: Texture Alphas + Feature #237: Run Morrowind-ini importer from launcher + Feature #286: Update Active Spell Icons + Feature #334: Swimming animation + Feature #335: Walking animation + Feature #360: Proper collision shapes for NPCs and creatures + Feature #367: Lights that behave more like original morrowind implementation + Feature #477: Special local scripting variables + Feature #528: Message boxes should close when enter is pressed under certain conditions. + Feature #543: Add bsa files to the settings imported by the ini importer + Feature #594: coordinate space and utility functions + Feature #625: Zoom in vanity mode + Task #464: Refactor launcher ESX selector into a re-usable component + Task #624: Unified implementation of type-variable sub-records + +0.21.0 +------ + + Bug #253: Dialogs don't work for Russian version of Morrowind + Bug #267: Activating creatures without dialogue can still activate the dialogue GUI + Bug #354: True flickering lights + Bug #386: The main menu's first entry is wrong (in french) + Bug #479: Adding the spell "Ash Woe Blight" to the player causes strange attribute oscillations + Bug #495: Activation Range + Bug #497: Failed Disposition check doesn't stop a dialogue entry from being returned + Bug #498: Failing a disposition check shouldn't eliminate topics from the the list of those available + Bug #500: Disposition for most NPCs is 0/100 + Bug #501: Getdisposition command wrongly returns base disposition + Bug #506: Journal UI doesn't update anymore + Bug #507: EnableRestMenu is not a valid command - change it to EnableRest + Bug #508: Crash in Ald Daedroth Shrine + Bug #517: Wrong price calculation when untrading an item + Bug #521: MWGui::InventoryWindow creates a duplicate player actor at the origin + Bug #524: Beast races are able to wear shoes + Bug #527: Background music fails to play + Bug #533: The arch at Gnisis entrance is not displayed + Bug #534: Terrain gets its correct shape only some time after the cell is loaded + Bug #536: The same entry can be added multiple times to the journal + Bug #539: Race selection is broken + Bug #544: Terrain normal map corrupt when the map is rendered + Feature #39: Video Playback + Feature #151: ^-escape sequences in text output + Feature #392: Add AI related script functions + Feature #456: Determine required ini fallback values and adjust the ini importer accordingly + Feature #460: Experimental DirArchives improvements + Feature #540: Execute scripts of objects in containers/inventories in active cells + Task #401: Review GMST fixing + Task #453: Unify case smashing/folding + Task #512: Rewrite utf8 component + +0.20.0 +------ + + Bug #366: Changing the player's race during character creation does not change the look of the player character + Bug #430: Teleporting and using loading doors linking within the same cell reloads the cell + Bug #437: Stop animations when paused + Bug #438: Time displays as "0 a.m." when it should be "12 a.m." + Bug #439: Text in "name" field of potion/spell creation window is persistent + Bug #440: Starting date at a new game is off by one day + Bug #442: Console window doesn't close properly sometimes + Bug #448: Do not break container window formatting when item names are very long + Bug #458: Topics sometimes not automatically added to known topic list + Bug #476: Auto-Moving allows player movement after using DisablePlayerControls + Bug #478: After sleeping in a bed the rest dialogue window opens automtically again + Bug #492: On creating potions the ingredients are removed twice + Feature #63: Mercantile skill + Feature #82: Persuasion Dialogue + Feature #219: Missing dialogue filters/functions + Feature #369: Add a FailedAction + Feature #377: Select head/hair on character creation + Feature #391: Dummy AI package classes + Feature #435: Global Map, 2nd Layer + Feature #450: Persuasion + Feature #457: Add more script instructions + Feature #474: update the global variable pcrace when the player's race is changed + Task #158: Move dynamically generated classes from Player class to World Class + Task #159: ESMStore rework and cleanup + Task #163: More Component Namespace Cleanup + Task #402: Move player data from MWWorld::Player to the player's NPC record + Task #446: Fix no namespace in BulletShapeLoader + +0.19.0 +------ + + Bug #374: Character shakes in 3rd person mode near the origin + Bug #404: Gamma correct rendering + Bug #407: Shoes of St. Rilm do not work + Bug #408: Rugs has collision even if they are not supposed to + Bug #412: Birthsign menu sorted incorrectly + Bug #413: Resolutions presented multiple times in launcher + Bug #414: launcher.cfg file stored in wrong directory + Bug #415: Wrong esm order in openmw.cfg + Bug #418: Sound listener position updates incorrectly + Bug #423: wrong usage of "Version" entry in openmw.desktop + Bug #426: Do not use hardcoded splash images + Bug #431: Don't use markers for raycast + Bug #432: Crash after picking up items from an NPC + Feature #21/#95: Sleeping/resting + Feature #61: Alchemy Skill + Feature #68: Death + Feature #69/#86: Spell Creation + Feature #72/#84: Travel + Feature #76: Global Map, 1st Layer + Feature #120: Trainer Window + Feature #152: Skill Increase from Skill Books + Feature #160: Record Saving + Task #400: Review GMST access + +0.18.0 +------ + + Bug #310: Button of the "preferences menu" are too small + Bug #361: Hand-to-hand skill is always 100 + Bug #365: NPC and creature animation is jerky; Characters float around when they are not supposed to + Bug #372: playSound3D uses original coordinates instead of current coordinates. + Bug #373: Static OGRE build faulty + Bug #375: Alt-tab toggle view + Bug #376: Screenshots are disable + Bug #378: Exception when drinking self-made potions + Bug #380: Cloth visibility problem + Bug #384: Weird character on doors tooltip. + Bug #398: Some objects do not collide in MW, but do so in OpenMW + Feature #22: Implement level-up + Feature #36: Hide Marker + Feature #88: Hotkey Window + Feature #91: Level-Up Dialogue + Feature #118: Keyboard and Mouse-Button bindings + Feature #119: Spell Buying Window + Feature #133: Handle resources across multiple data directories + Feature #134: Generate a suitable default-value for --data-local + Feature #292: Object Movement/Creation Script Instructions + Feature #340: AIPackage data structures + Feature #356: Ingredients use + Feature #358: Input system rewrite + Feature #370: Target handling in actions + Feature #379: Door markers on the local map + Feature #389: AI framework + Feature #395: Using keys to open doors / containers + Feature #396: Loading screens + Feature #397: Inventory avatar image and race selection head preview + Task #339: Move sounds into Action + +0.17.0 +------ + + Bug #225: Valgrind reports about 40MB of leaked memory + Bug #241: Some physics meshes still don't match + Bug #248: Some textures are too dark + Bug #300: Dependency on proprietary CG toolkit + Bug #302: Some objects don't collide although they should + Bug #308: Freeze in Balmora, Meldor: Armorer + Bug #313: openmw without a ~/.config/openmw folder segfault. + Bug #317: adding non-existing spell via console locks game + Bug #318: Wrong character normals + Bug #341: Building with Ogre Debug libraries does not use debug version of plugins + Bug #347: Crash when running openmw with --start="XYZ" + Bug #353: FindMyGUI.cmake breaks path on Windows + Bug #359: WindowManager throws exception at destruction + Bug #364: Laggy input on OS X due to bug in Ogre's event pump implementation + Feature #33: Allow objects to cross cell-borders + Feature #59: Dropping Items (replaced stopgap implementation with a proper one) + Feature #93: Main Menu + Feature #96/329/330/331/332/333: Player Control + Feature #180: Object rotation and scaling. + Feature #272: Incorrect NIF material sharing + Feature #314: Potion usage + Feature #324: Skill Gain + Feature #342: Drain/fortify dynamic stats/attributes magic effects + Feature #350: Allow console only script instructions + Feature #352: Run scripts in console on startup + Task #107: Refactor mw*-subsystems + Task #325: Make CreatureStats into a class + Task #345: Use Ogre's animation system + Task #351: Rewrite Action class to support automatic sound playing + +0.16.0 +------ + + Bug #250: OpenMW launcher erratic behaviour + Bug #270: Crash because of underwater effect on OS X + Bug #277: Auto-equipping in some cells not working + Bug #294: Container GUI ignores disabled inventory menu + Bug #297: Stats review dialog shows all skills and attribute values as 0 + Bug #298: MechanicsManager::buildPlayer does not remove previous bonuses + Bug #299: Crash in World::disable + Bug #306: Non-existent ~/.config/openmw "crash" the launcher. + Bug #307: False "Data Files" location make the launcher "crash" + Feature #81: Spell Window + Feature #85: Alchemy Window + Feature #181: Support for x.y script syntax + Feature #242: Weapon and Spell icons + Feature #254: Ingame settings window + Feature #293: Allow "stacking" game modes + Feature #295: Class creation dialog tooltips + Feature #296: Clicking on the HUD elements should show/hide the respective window + Feature #301: Direction after using a Teleport Door + Feature #303: Allow object selection in the console + Feature #305: Allow the use of = as a synonym for == + Feature #312: Compensation for slow object access in poorly written Morrowind.esm scripts + Task #176: Restructure enabling/disabling of MW-references + Task #283: Integrate ogre.cfg file in settings file + Task #290: Auto-Close MW-reference related GUI windows + +0.15.0 +------ + + Bug #5: Physics reimplementation (fixes various issues) + Bug #258: Resizing arrow's background is not transparent + Bug #268: Widening the stats window in X direction causes layout problems + Bug #269: Topic pane in dialgoue window is too small for some longer topics + Bug #271: Dialog choices are sorted incorrectly + Bug #281: The single quote character is not rendered on dialog windows + Bug #285: Terrain not handled properly in cells that are not predefined + Bug #289: Dialogue filter isn't doing case smashing/folding for item IDs + Feature #15: Collision with Terrain + Feature #17: Inventory-, Container- and Trade-Windows + Feature #44: Floating Labels above Focussed Objects + Feature #80: Tooltips + Feature #83: Barter Dialogue + Feature #90: Book and Scroll Windows + Feature #156: Item Stacking in Containers + Feature #213: Pulsating lights + Feature #218: Feather & Burden + Feature #256: Implement magic effect bookkeeping + Feature #259: Add missing information to Stats window + Feature #260: Correct case for dialogue topics + Feature #280: GUI texture atlasing + Feature #291: Ability to use GMST strings from GUI layout files + Task #255: Make MWWorld::Environment into a singleton + +0.14.0 +------ + + Bug #1: Meshes rendered with wrong orientation + Bug #6/Task #220: Picking up small objects doesn't always work + Bug #127: tcg doesn't work + Bug #178: Compablity problems with Ogre 1.8.0 RC 1 + Bug #211: Wireframe mode (toggleWireframe command) should not apply to Console & other UI + Bug #227: Terrain crashes when moving away from predefined cells + Bug #229: On OS X Launcher cannot launch game if path to binary contains spaces + Bug #235: TGA texture loading problem + Bug #246: wireframe mode does not work in water + Feature #8/#232: Water Rendering + Feature #13: Terrain Rendering + Feature #37: Render Path Grid + Feature #66: Factions + Feature #77: Local Map + Feature #78: Compass/Mini-Map + Feature #97: Render Clothing/Armour + Feature #121: Window Pinning + Feature #205: Auto equip + Feature #217: Contiainer should track changes to its content + Feature #221: NPC Dialogue Window Enhancements + Feature #233: Game settings manager + Feature #240: Spell List and selected spell (no GUI yet) + Feature #243: Draw State + Task #113: Morrowind.ini Importer + Task #215: Refactor the sound code + Task #216: Update MyGUI + +0.13.0 +------ + + Bug #145: Fixed sound problems after cell change + Bug #179: Pressing space in console triggers activation + Bug #186: CMake doesn't use the debug versions of Ogre libraries on Linux + Bug #189: ASCII 16 character added to console on it's activation on Mac OS X + Bug #190: Case Folding fails with music files + Bug #192: Keypresses write Text into Console no matter which gui element is active + Bug #196: Collision shapes out of place + Bug #202: ESMTool doesn't not work with localised ESM files anymore + Bug #203: Torch lights only visible on short distance + Bug #207: Ogre.log not written + Bug #209: Sounds do not play + Bug #210: Ogre crash at Dren plantation + Bug #214: Unsupported file format version + Bug #222: Launcher is writing openmw.cfg file to wrong location + Feature #9: NPC Dialogue Window + Feature #16/42: New sky/weather implementation + Feature #40: Fading + Feature #48: NPC Dialogue System + Feature #117: Equipping Items (backend only, no GUI yet, no rendering of equipped items yet) + Feature #161: Load REC_PGRD records + Feature #195: Wireframe-mode + Feature #198/199: Various sound effects + Feature #206: Allow picking data path from launcher if non is set + Task #108: Refactor window manager class + Task #172: Sound Manager Cleanup + Task #173: Create OpenEngine systems in the appropriate manager classes + Task #184: Adjust MSVC and gcc warning levels + Task #185: RefData rewrite + Task #201: Workaround for transparency issues + Task #208: silenced esm_reader.hpp warning + +0.12.0 +------ + + Bug #154: FPS Drop + Bug #169: Local scripts continue running if associated object is deleted + Bug #174: OpenMW fails to start if the config directory doesn't exist + Bug #187: Missing lighting + Bug #188: Lights without a mesh are not rendered + Bug #191: Taking screenshot causes crash when running installed + Feature #28: Sort out the cell load problem + Feature #31: Allow the player to move away from pre-defined cells + Feature #35: Use alternate storage location for modified object position + Feature #45: NPC animations + Feature #46: Creature Animation + Feature #89: Basic Journal Window + Feature #110: Automatically pick up the path of existing MW-installations + Feature #183: More FPS display settings + Task #19: Refactor engine class + Task #109/Feature #162: Automate Packaging + Task #112: Catch exceptions thrown in input handling functions + Task #128/#168: Cleanup Configuration File Handling + Task #131: NPC Activation doesn't work properly + Task #144: MWRender cleanup + Task #155: cmake cleanup + +0.11.1 +------ + + Bug #2: Resources loading doesn't work outside of bsa files + Bug #3: GUI does not render non-English characters + Bug #7: openmw.cfg location doesn't match + Bug #124: The TCL alias for ToggleCollision is missing. + Bug #125: Some command line options can't be used from a .cfg file + Bug #126: Toggle-type script instructions are less verbose compared with original MW + Bug #130: NPC-Record Loading fails for some NPCs + Bug #167: Launcher sets invalid parameters in ogre config + Feature #10: Journal + Feature #12: Rendering Optimisations + Feature #23: Change Launcher GUI to a tabbed interface + Feature #24: Integrate the OGRE settings window into the launcher + Feature #25: Determine openmw.cfg location (Launcher) + Feature #26: Launcher Profiles + Feature #79: MessageBox + Feature #116: Tab-Completion in Console + Feature #132: --data-local and multiple --data + Feature #143: Non-Rendering Performance-Optimisations + Feature #150: Accessing objects in cells via ID does only work for objects with all lower case IDs + Feature #157: Version Handling + Task #14: Replace tabs with 4 spaces + Task #18: Move components from global namespace into their own namespace + Task #123: refactor header files in components/esm + +0.10.0 +------ + +* NPC dialogue window (not functional yet) +* Collisions with objects +* Refactor the PlayerPos class +* Adjust file locations +* CMake files and test linking for Bullet +* Replace Ogre raycasting test for activation with something more precise +* Adjust player movement according to collision results +* FPS display +* Various Portability Improvements +* Mac OS X support is back! + +0.9.0 +----- + +* Exterior cells loading, unloading and management +* Character Creation GUI +* Character creation +* Make cell names case insensitive when doing internal lookups +* Music player +* NPCs rendering + +0.8.0 +----- + +* GUI +* Complete and working script engine +* In game console +* Sky rendering +* Sound and music +* Tons of smaller stuff + +0.7.0 +----- + +* This release is a complete rewrite in C++. +* All D code has been culled, and all modules have been rewritten. +* The game is now back up to the level of rendering interior cells and moving around, but physics, sound, GUI, and scripting still remain to be ported from the old codebase. + +0.6.0 +----- + +* Coded a GUI system using MyGUI +* Skinned MyGUI to look like Morrowind (work in progress) +* Integrated the Monster script engine +* Rewrote some functions into script code +* Very early MyGUI < > Monster binding +* Fixed Windows sound problems (replaced old openal32.dll) + +0.5.0 +----- + +* Collision detection with Bullet +* Experimental walk & fall character physics +* New key bindings: + * t toggle physics mode (walking, flying, ghost), + * n night eye, brightens the scene +* Fixed incompatability with DMD 1.032 and newer compilers +* * (thanks to tomqyp) +* Various minor changes and updates + +0.4.0 +----- + +* Switched from Audiere to OpenAL +* * (BIG thanks to Chris Robinson) +* Added complete Makefile (again) as a alternative build tool +* More realistic lighting (thanks again to Chris Robinson) +* Various localization fixes tested with Russian and French versions +* Temporary workaround for the Unicode issue: invalid UTF displayed as '?' +* Added ns option to disable sound, for debugging +* Various bug fixes +* Cosmetic changes to placate gdc Wall + +0.3.0 +----- + +* Built and tested on Windows XP +* Partial support for FreeBSD (exceptions do not work) +* You no longer have to download Monster separately +* Made an alternative for building without DSSS (but DSSS still works) +* Renamed main program from 'morro' to 'openmw' +* Made the config system more robust +* Added oc switch for showing Ogre config window on startup +* Removed some config files, these are auto generated when missing. +* Separated plugins.cfg into linux and windows versions. +* Updated Makefile and sources for increased portability +* confirmed to work against OIS 1.0.0 (Ubuntu repository package) + +0.2.0 +----- + +* Compiles with gdc +* Switched to DSSS for building D code +* Includes the program esmtool + +0.1.0 +----- + +first release diff --git a/CI/before_install.linux.sh b/CI/before_install.linux.sh new file mode 100755 index 000000000..998c285db --- /dev/null +++ b/CI/before_install.linux.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +export CXX=g++ +export CC=gcc + +echo "yes" | sudo add-apt-repository "deb http://archive.ubuntu.com/ubuntu `lsb_release -sc` main universe restricted multiverse" +echo "yes" | sudo apt-add-repository ppa:openmw/openmw +sudo apt-get update -qq +sudo apt-get install -qq libgtest-dev google-mock +sudo apt-get install -qq libboost-filesystem-dev libboost-program-options-dev libboost-system-dev libboost-thread-dev libboost-wave-dev +sudo apt-get install -qq libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libavresample-dev +sudo apt-get install -qq libbullet-dev libogre-1.9-dev libmygui-dev libsdl2-dev libunshield-dev libtinyxml-dev libopenal-dev libqt4-dev +sudo mkdir /usr/src/gtest/build +cd /usr/src/gtest/build +sudo cmake .. -DBUILD_SHARED_LIBS=1 +sudo make -j4 +sudo ln -s /usr/src/gtest/build/libgtest.so /usr/lib/libgtest.so +sudo ln -s /usr/src/gtest/build/libgtest_main.so /usr/lib/libgtest_main.so diff --git a/CI/before_install.osx.sh b/CI/before_install.osx.sh new file mode 100755 index 000000000..8bfe2b70f --- /dev/null +++ b/CI/before_install.osx.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +export CXX=clang++ +export CC=clang + +brew tap openmw/openmw +brew update +brew unlink boost +brew install openmw-mygui openmw-bullet openmw-sdl2 openmw-ffmpeg openmw/openmw/qt unshield diff --git a/CI/before_script.linux.sh b/CI/before_script.linux.sh new file mode 100755 index 000000000..b4889c9e1 --- /dev/null +++ b/CI/before_script.linux.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +mkdir build +cd build +cmake .. -DBUILD_WITH_CODE_COVERAGE=1 -DBUILD_UNITTESTS=1 -DCMAKE_INSTALL_PREFIX=/usr -DBINDIR=/usr/games -DCMAKE_BUILD_TYPE="RelWithDebInfo" -DUSE_SYSTEM_TINYXML=TRUE diff --git a/CI/before_script.osx.sh b/CI/before_script.osx.sh new file mode 100755 index 000000000..772284f91 --- /dev/null +++ b/CI/before_script.osx.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +mkdir build +cd build +cmake -DCMAKE_FRAMEWORK_PATH="/usr/local/lib/macosx/Release" -DCMAKE_EXE_LINKER_FLAGS="-F/usr/local/lib/macosx/Release" -DCMAKE_CXX_FLAGS="-stdlib=libstdc++" -DCMAKE_BUILD_TYPE=Debug -DBUILD_MYGUI_PLUGIN=OFF -G"Unix Makefiles" .. diff --git a/CMakeLists.txt b/CMakeLists.txt index ee3850ddb..b01525692 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,12 @@ project(OpenMW) +# If the user doesn't supply a CMAKE_BUILD_TYPE via command line, choose one for them. +IF(NOT CMAKE_BUILD_TYPE) + SET(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING + "Choose the type of build, options are: Debug Release RelWithDebInfo MinSizeRel." + FORCE) +ENDIF() + if (APPLE) set(APP_BUNDLE_NAME "${CMAKE_PROJECT_NAME}.app") @@ -12,7 +19,7 @@ set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake/) message(STATUS "Configuring OpenMW...") set(OPENMW_VERSION_MAJOR 0) -set(OPENMW_VERSION_MINOR 31) +set(OPENMW_VERSION_MINOR 35) set(OPENMW_VERSION_RELEASE 0) set(OPENMW_VERSION_COMMITHASH "") @@ -20,29 +27,13 @@ set(OPENMW_VERSION_TAGHASH "") set(OPENMW_VERSION "${OPENMW_VERSION_MAJOR}.${OPENMW_VERSION_MINOR}.${OPENMW_VERSION_RELEASE}") +set(GIT_CHECKOUT FALSE) if(EXISTS ${PROJECT_SOURCE_DIR}/.git) if(NOT EXISTS ${PROJECT_SOURCE_DIR}/.git/shallow) find_package(Git) if(GIT_FOUND) - include(GetGitRevisionDescription) - get_git_tag_revision(TAGHASH --tags --max-count=1) - get_git_head_revision(REFSPEC COMMITHASH) - git_describe(VERSION --tags ${TAGHASH}) - - string(REGEX MATCH "^openmw-[^0-9]*[0-9]+\\.[0-9]+\\.[0-9]+.*" MATCH "${VERSION}") - if(MATCH) - string(REGEX REPLACE "^openmw-([0-9]+)\\..*" "\\1" GIT_VERSION_MAJOR "${VERSION}") - string(REGEX REPLACE "^openmw-[0-9]+\\.([0-9]+).*" "\\1" GIT_VERSION_MINOR "${VERSION}") - string(REGEX REPLACE "^openmw-[0-9]+\\.[0-9]+\\.([0-9]+).*" "\\1" GIT_VERSION_RELEASE "${VERSION}") - - set(OPENMW_VERSION_COMMITHASH "${COMMITHASH}") - set(OPENMW_VERSION_TAGHASH "${TAGHASH}") - - message(STATUS "OpenMW version ${OPENMW_VERSION}") - else(MATCH) - message(WARNING "Failed to get valid version information from Git") - endif(MATCH) + set(GIT_CHECKOUT TRUE) else(GIT_FOUND) message(WARNING "Git executable not found") endif(GIT_FOUND) @@ -54,28 +45,35 @@ endif(EXISTS ${PROJECT_SOURCE_DIR}/.git) # Macros include(OpenMWMacros) +if (ANDROID) + set(CMAKE_FIND_ROOT_PATH ${OPENMW_DEPENDENCIES_DIR} "${CMAKE_FIND_ROOT_PATH}") +endif (ANDROID) + # doxygen main page -configure_file ("${OpenMW_SOURCE_DIR}/docs/mainpage.hpp.cmake" "${OpenMW_SOURCE_DIR}/docs/mainpage.hpp") +configure_file ("${OpenMW_SOURCE_DIR}/docs/mainpage.hpp.cmake" "${OpenMW_BINARY_DIR}/docs/mainpage.hpp") option(MYGUI_STATIC "Link static build of Mygui into the binaries" FALSE) option(OGRE_STATIC "Link static build of Ogre and Ogre Plugins into the binaries" FALSE) option(BOOST_STATIC "Link static build of Boost into the binaries" FALSE) option(SDL2_STATIC "Link static build of SDL into the binaries" FALSE) +set(CUSTOM_OGRE_PLUGIN_DIR "" CACHE PATH "Specify a custom directory for Ogre plugins (autodetected by default)") + option(OPENMW_UNITY_BUILD "Use fewer compilation units to speed up compile time" FALSE) # Apps and tools -option(BUILD_BSATOOL "build BSA extractor" OFF) +option(BUILD_BSATOOL "build BSA extractor" ON) option(BUILD_ESMTOOL "build ESM inspector" ON) option(BUILD_LAUNCHER "build Launcher" ON) option(BUILD_MWINIIMPORTER "build MWiniImporter" ON) +option(BUILD_ESSIMPORTER "build ESS (Morrowind save game) importer" ON) option(BUILD_OPENCS "build OpenMW Construction Set" ON) +option(BUILD_WIZARD "build Installation Wizard" ON) option(BUILD_WITH_CODE_COVERAGE "Enable code coverage with gconv" OFF) -option(BUILD_UNITTESTS "Enable Unittests with Google C++ Unittest ang GMock frameworks" OFF) - -# Sound source selection -option(USE_FFMPEG "use ffmpeg for sound" ON) +option(BUILD_UNITTESTS "Enable Unittests with Google C++ Unittest" OFF) +option(BUILD_NIFTEST "build nif file tester" OFF) +option(BUILD_MYGUI_PLUGIN "build MyGUI plugin for OpenMW resources, to use with MyGUI tools" ON) # OS X deployment option(OPENMW_OSX_DEPLOYMENT OFF) @@ -101,33 +99,32 @@ cmake_minimum_required(VERSION 2.6) # source directory: libs -set(LIBDIR ${CMAKE_SOURCE_DIR}/libs) +set(LIBS_DIR ${CMAKE_SOURCE_DIR}/libs) set(OENGINE_OGRE - ${LIBDIR}/openengine/ogre/renderer.cpp - ${LIBDIR}/openengine/ogre/fader.cpp - ${LIBDIR}/openengine/ogre/lights.cpp - ${LIBDIR}/openengine/ogre/selectionbuffer.cpp - ${LIBDIR}/openengine/ogre/imagerotate.cpp + ${LIBS_DIR}/openengine/ogre/renderer.cpp + ${LIBS_DIR}/openengine/ogre/lights.cpp + ${LIBS_DIR}/openengine/ogre/selectionbuffer.cpp + ${LIBS_DIR}/openengine/ogre/imagerotate.cpp ) set(OENGINE_GUI - ${LIBDIR}/openengine/gui/loglistener.cpp - ${LIBDIR}/openengine/gui/manager.cpp - ${LIBDIR}/openengine/gui/layout.hpp + ${LIBS_DIR}/openengine/gui/loglistener.cpp + ${LIBS_DIR}/openengine/gui/manager.cpp + ${LIBS_DIR}/openengine/gui/layout.cpp ) set(OENGINE_BULLET - ${LIBDIR}/openengine/bullet/BtOgre.cpp - ${LIBDIR}/openengine/bullet/BtOgreExtras.h - ${LIBDIR}/openengine/bullet/BtOgreGP.h - ${LIBDIR}/openengine/bullet/BtOgrePG.h - ${LIBDIR}/openengine/bullet/physic.cpp - ${LIBDIR}/openengine/bullet/physic.hpp - ${LIBDIR}/openengine/bullet/BulletShapeLoader.cpp - ${LIBDIR}/openengine/bullet/BulletShapeLoader.h - ${LIBDIR}/openengine/bullet/trace.cpp - ${LIBDIR}/openengine/bullet/trace.h + ${LIBS_DIR}/openengine/bullet/BtOgre.cpp + ${LIBS_DIR}/openengine/bullet/BtOgreExtras.h + ${LIBS_DIR}/openengine/bullet/BtOgreGP.h + ${LIBS_DIR}/openengine/bullet/BtOgrePG.h + ${LIBS_DIR}/openengine/bullet/physic.cpp + ${LIBS_DIR}/openengine/bullet/physic.hpp + ${LIBS_DIR}/openengine/bullet/BulletShapeLoader.cpp + ${LIBS_DIR}/openengine/bullet/BulletShapeLoader.h + ${LIBS_DIR}/openengine/bullet/trace.cpp + ${LIBS_DIR}/openengine/bullet/trace.h ) @@ -138,32 +135,24 @@ set(OPENMW_LIBS ${OENGINE_ALL}) set(OPENMW_LIBS_HEADER) # Sound setup -set(GOT_SOUND_INPUT 0) -set(SOUND_INPUT_INCLUDES "") -set(SOUND_INPUT_LIBRARY "") -set(SOUND_DEFINE "") -if (USE_FFMPEG) - set(FFmpeg_FIND_COMPONENTS AVCODEC AVFORMAT AVUTIL SWSCALE) - find_package(FFmpeg) - if (FFMPEG_FOUND) - set(SOUND_INPUT_INCLUDES ${SOUND_INPUT_INCLUDES} ${FFMPEG_INCLUDE_DIRS}) - set(SOUND_INPUT_LIBRARY ${SOUND_INPUT_LIBRARY} ${FFMPEG_LIBRARIES} ${SWSCALE_LIBRARIES}) - set(SOUND_DEFINE ${SOUND_DEFINE} -DOPENMW_USE_FFMPEG) - set(GOT_SOUND_INPUT 1) - endif (FFMPEG_FOUND) -endif (USE_FFMPEG) - -if (NOT GOT_SOUND_INPUT) - message(WARNING "--------------------") - message(WARNING "Failed to find any sound input packages") - message(WARNING "--------------------") -endif (NOT GOT_SOUND_INPUT) - -if (NOT FFMPEG_FOUND) - message(WARNING "--------------------") - message(WARNING "FFmpeg not found, video playback will be disabled") - message(WARNING "--------------------") -endif (NOT FFMPEG_FOUND) +set(FFmpeg_FIND_COMPONENTS AVCODEC AVFORMAT AVUTIL SWSCALE SWRESAMPLE AVRESAMPLE) +unset(FFMPEG_LIBRARIES CACHE) +find_package(FFmpeg) +if ( NOT AVCODEC_FOUND OR NOT AVFORMAT_FOUND OR NOT AVUTIL_FOUND OR NOT SWSCALE_FOUND ) + message(FATAL_ERROR "FFmpeg component required, but not found!") +endif() +set(SOUND_INPUT_INCLUDES ${FFMPEG_INCLUDE_DIRS}) +set(SOUND_INPUT_LIBRARY ${FFMPEG_LIBRARIES} ${SWSCALE_LIBRARIES}) +if( SWRESAMPLE_FOUND ) + add_definitions(-DHAVE_LIBSWRESAMPLE) + set(SOUND_INPUT_LIBRARY ${FFMPEG_LIBRARIES} ${SWRESAMPLE_LIBRARIES}) +else() + if( AVRESAMPLE_FOUND ) + set(SOUND_INPUT_LIBRARY ${FFMPEG_LIBRARIES} ${AVRESAMPLE_LIBRARIES}) + else() + message(FATAL_ERROR "Install either libswresample (FFmpeg) or libavresample (Libav).") + endif() +endif() # TinyXML option(USE_SYSTEM_TINYXML "Use system TinyXML library instead of internal." OFF) @@ -227,7 +216,15 @@ IF(BOOST_STATIC) endif() find_package(OGRE REQUIRED) +if (${OGRE_VERSION} VERSION_LESS "1.9") + message(FATAL_ERROR "OpenMW requires Ogre 1.9 or later, please install the latest stable version from http://ogre3d.org") +endif() + find_package(MyGUI REQUIRED) +if (${MYGUI_VERSION} VERSION_LESS "3.2.1") + message(FATAL_ERROR "OpenMW requires MyGUI 3.2.1 or later, please install the latest version from http://mygui.info") +endif() + find_package(Boost REQUIRED COMPONENTS ${BOOST_COMPONENTS}) find_package(SDL2 REQUIRED) find_package(OpenAL REQUIRED) @@ -258,7 +255,12 @@ if(OGRE_STATIC) list(APPEND OGRE_STATIC_PLUGINS ${Cg_LIBRARIES}) endif(Cg_FOUND) +if (ANDROID) + add_static_ogre_plugin(RenderSystem_GLES2) +else () add_static_ogre_plugin(RenderSystem_GL) +endif () + if(WIN32) add_static_ogre_plugin(RenderSystem_Direct3D9) endif(WIN32) @@ -266,13 +268,14 @@ endif(OGRE_STATIC) include_directories("." ${OGRE_INCLUDE_DIR} ${OGRE_INCLUDE_DIR}/Ogre ${OGRE_INCLUDE_DIR}/OGRE ${OGRE_INCLUDE_DIRS} ${OGRE_PLUGIN_INCLUDE_DIRS} + ${OGRE_INCLUDE_DIR}/Overlay ${OGRE_Overlay_INCLUDE_DIR} ${SDL2_INCLUDE_DIR} ${Boost_INCLUDE_DIR} ${PLATFORM_INCLUDE_DIR} ${MYGUI_INCLUDE_DIRS} ${MYGUI_PLATFORM_INCLUDE_DIRS} ${OPENAL_INCLUDE_DIR} - ${LIBDIR} + ${LIBS_DIR} ) link_directories(${SDL2_LIBRARY_DIRS} ${Boost_LIBRARY_DIRS} ${OGRE_LIB_DIR} ${MYGUI_LIB_DIR}) @@ -329,8 +332,10 @@ if (APPLE AND OPENMW_OSX_DEPLOYMENT) # make it empty so plugin loading code can check this and try to find plugins inside app bundle add_definitions(-DOGRE_PLUGIN_DIR="") else() - if (NOT DEFINED ${OGRE_PLUGIN_DIR}) + if (CUSTOM_OGRE_PLUGIN_DIR STREQUAL "") set(OGRE_PLUGIN_DIR ${OGRE_PLUGIN_DIR_REL}) + else() + set(OGRE_PLUGIN_DIR ${CUSTOM_OGRE_PLUGIN_DIR}) endif() add_definitions(-DOGRE_PLUGIN_DIR="${OGRE_PLUGIN_DIR}") @@ -344,8 +349,10 @@ add_subdirectory(files/mygui) if (APPLE) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${APP_BUNDLE_DIR}/Contents/MacOS") + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${APP_BUNDLE_DIR}/Contents/MacOS") else (APPLE) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${OpenMW_BINARY_DIR}") + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${OpenMW_BINARY_DIR}") endif (APPLE) # Other files @@ -368,41 +375,54 @@ configure_file(${OpenMW_SOURCE_DIR}/files/opencs.ini configure_file(${OpenMW_SOURCE_DIR}/files/opencs/defaultfilters "${OpenMW_BINARY_DIR}/resources/defaultfilters" COPYONLY) +configure_file(${OpenMW_SOURCE_DIR}/files/gamecontrollerdb.txt + "${OpenMW_BINARY_DIR}/gamecontrollerdb.txt") + if (NOT WIN32 AND NOT APPLE) configure_file(${OpenMW_SOURCE_DIR}/files/openmw.desktop "${OpenMW_BINARY_DIR}/openmw.desktop") - configure_file(${OpenMW_SOURCE_DIR}/files/opencs.desktop - "${OpenMW_BINARY_DIR}/opencs.desktop") + configure_file(${OpenMW_SOURCE_DIR}/files/openmw-cs.desktop + "${OpenMW_BINARY_DIR}/openmw-cs.desktop") endif() # Compiler settings if (CMAKE_COMPILER_IS_GNUCC) - SET(CMAKE_CXX_FLAGS "-Wall -Wextra -Wno-unused-parameter -Wno-reorder -std=c++98 -pedantic -Wno-long-long ${CMAKE_CXX_FLAGS}") - - # Silence warnings in OGRE headers. Remove once OGRE got fixed! - SET(CMAKE_CXX_FLAGS "-Wno-ignored-qualifiers ${CMAKE_CXX_FLAGS}") + set_property(GLOBAL APPEND_STRING PROPERTY COMPILE_FLAGS "-Wall -Wextra -Wno-unused-parameter -Wno-reorder -std=c++98 -pedantic -Wno-long-long") execute_process(COMMAND ${CMAKE_C_COMPILER} -dumpversion OUTPUT_VARIABLE GCC_VERSION) if ("${GCC_VERSION}" VERSION_GREATER 4.6 OR "${GCC_VERSION}" VERSION_EQUAL 4.6) - SET(CMAKE_CXX_FLAGS "-Wno-unused-but-set-parameter ${CMAKE_CXX_FLAGS}") + set_property(GLOBAL APPEND_STRING PROPERTY COMPILE_FLAGS "-Wno-unused-but-set-parameter") endif("${GCC_VERSION}" VERSION_GREATER 4.6 OR "${GCC_VERSION}" VERSION_EQUAL 4.6) +elseif (MSVC) + # Enable link-time code generation globally for all linking + set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /GL") + set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} /LTCG") + set(CMAKE_SHARED_LINKER_FLAGS_RELEASE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} /LTCG") + set(CMAKE_STATIC_LINKER_FLAGS_RELEASE "${CMAKE_STATIC_LINKER_FLAGS_RELEASE} /LTCG") endif (CMAKE_COMPILER_IS_GNUCC) IF(NOT WIN32 AND NOT APPLE) # Linux building # Paths SET(BINDIR "${CMAKE_INSTALL_PREFIX}/bin" CACHE PATH "Where to install binaries") + SET(LIBDIR "${CMAKE_INSTALL_PREFIX}/lib${LIB_SUFFIX}" CACHE PATH "Where to install libraries") SET(DATAROOTDIR "${CMAKE_INSTALL_PREFIX}/share" CACHE PATH "Sets the root of data directories to a non-default location") - SET(DATADIR "${DATAROOTDIR}/games/openmw" CACHE PATH "Sets the openmw data directories to a non-default location") + SET(GLOBAL_DATA_PATH "${DATAROOTDIR}/games/" CACHE PATH "Set data path prefix") + SET(DATADIR "${GLOBAL_DATA_PATH}/openmw" CACHE PATH "Sets the openmw data directories to a non-default location") SET(ICONDIR "${DATAROOTDIR}/pixmaps" CACHE PATH "Set icon dir") SET(LICDIR "${DATAROOTDIR}/licenses/openmw" CACHE PATH "Sets the openmw license directory to a non-default location.") - SET(SYSCONFDIR "/etc/openmw" CACHE PATH "Set config dir") + IF("${CMAKE_INSTALL_PREFIX}" STREQUAL "/usr") + SET(GLOBAL_CONFIG_PATH "/etc/" CACHE PATH "Set config dir prefix") + ELSE() + SET(GLOBAL_CONFIG_PATH "${CMAKE_INSTALL_PREFIX}/etc/" CACHE PATH "Set config dir prefix") + ENDIF() + SET(SYSCONFDIR "${GLOBAL_CONFIG_PATH}/openmw" CACHE PATH "Set config dir") # Install binaries INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/openmw" DESTINATION "${BINDIR}" ) IF(BUILD_LAUNCHER) - INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/omwlauncher" DESTINATION "${BINDIR}" ) + INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/openmw-launcher" DESTINATION "${BINDIR}" ) ENDIF(BUILD_LAUNCHER) IF(BUILD_BSATOOL) INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/bsatool" DESTINATION "${BINDIR}" ) @@ -411,34 +431,48 @@ IF(NOT WIN32 AND NOT APPLE) INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/esmtool" DESTINATION "${BINDIR}" ) ENDIF(BUILD_ESMTOOL) IF(BUILD_MWINIIMPORTER) - INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/mwiniimport" DESTINATION "${BINDIR}" ) + INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/openmw-iniimporter" DESTINATION "${BINDIR}" ) ENDIF(BUILD_MWINIIMPORTER) + IF(BUILD_ESSIMPORTER) + INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/openmw-essimporter" DESTINATION "${BINDIR}" ) + ENDIF(BUILD_ESSIMPORTER) IF(BUILD_OPENCS) - INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/opencs" DESTINATION "${BINDIR}" ) + INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/openmw-cs" DESTINATION "${BINDIR}" ) ENDIF(BUILD_OPENCS) + IF(BUILD_NIFTEST) + INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/niftest" DESTINATION "${BINDIR}" ) + ENDIF(BUILD_NIFTEST) + IF(BUILD_WIZARD) + INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/openmw-wizard" DESTINATION "${BINDIR}" ) + ENDIF(BUILD_WIZARD) + if(BUILD_MYGUI_PLUGIN) + INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/Plugin_MyGUI_OpenMW_Resources.so" DESTINATION "${LIBDIR}" ) + ENDIF(BUILD_MYGUI_PLUGIN) # Install licenses INSTALL(FILES "docs/license/DejaVu Font License.txt" DESTINATION "${LICDIR}" ) INSTALL(FILES "extern/shiny/License.txt" DESTINATION "${LICDIR}" RENAME "Shiny License.txt" ) # Install icon and desktop file - INSTALL(FILES "${OpenMW_BINARY_DIR}/openmw.desktop" DESTINATION "${DATAROOTDIR}/applications" PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ COMPONENT "openmw") - INSTALL(FILES "${OpenMW_SOURCE_DIR}/files/launcher/images/openmw.png" DESTINATION "${ICONDIR}" PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ COMPONENT "openmw") + INSTALL(FILES "${OpenMW_BINARY_DIR}/openmw.desktop" DESTINATION "${DATAROOTDIR}/applications" COMPONENT "openmw") + INSTALL(FILES "${OpenMW_SOURCE_DIR}/files/launcher/images/openmw.png" DESTINATION "${ICONDIR}" COMPONENT "openmw") IF(BUILD_OPENCS) - INSTALL(FILES "${OpenMW_BINARY_DIR}/opencs.desktop" DESTINATION "${DATAROOTDIR}/applications" PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ COMPONENT "opencs") - INSTALL(FILES "${OpenMW_SOURCE_DIR}/files/opencs/opencs.png" DESTINATION "${ICONDIR}" PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ COMPONENT "opencs") + INSTALL(FILES "${OpenMW_BINARY_DIR}/openmw-cs.desktop" DESTINATION "${DATAROOTDIR}/applications" COMPONENT "opencs") + INSTALL(FILES "${OpenMW_SOURCE_DIR}/files/opencs/openmw-cs.png" DESTINATION "${ICONDIR}" COMPONENT "opencs") ENDIF(BUILD_OPENCS) # Install global configuration files - INSTALL(FILES "${OpenMW_BINARY_DIR}/settings-default.cfg" DESTINATION "${SYSCONFDIR}" PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ COMPONENT "openmw") - INSTALL(FILES "${OpenMW_BINARY_DIR}/transparency-overrides.cfg" DESTINATION "${SYSCONFDIR}" PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ COMPONENT "openmw") - INSTALL(FILES "${OpenMW_BINARY_DIR}/openmw.cfg.install" DESTINATION "${SYSCONFDIR}" RENAME "openmw.cfg" PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ COMPONENT "openmw") + INSTALL(FILES "${OpenMW_BINARY_DIR}/settings-default.cfg" DESTINATION "${SYSCONFDIR}" COMPONENT "openmw") + INSTALL(FILES "${OpenMW_BINARY_DIR}/transparency-overrides.cfg" DESTINATION "${SYSCONFDIR}" COMPONENT "openmw") + INSTALL(FILES "${OpenMW_BINARY_DIR}/openmw.cfg.install" DESTINATION "${SYSCONFDIR}" RENAME "openmw.cfg" COMPONENT "openmw") + INSTALL(FILES "${OpenMW_BINARY_DIR}/gamecontrollerdb.txt" DESTINATION "${SYSCONFDIR}" COMPONENT "openmw") + IF(BUILD_OPENCS) - INSTALL(FILES "${OpenMW_BINARY_DIR}/opencs.ini" DESTINATION "${SYSCONFDIR}" PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ COMPONENT "opencs") + INSTALL(FILES "${OpenMW_BINARY_DIR}/opencs.ini" DESTINATION "${SYSCONFDIR}" COMPONENT "opencs") ENDIF(BUILD_OPENCS) # Install resources - INSTALL(DIRECTORY "${OpenMW_BINARY_DIR}/resources" DESTINATION "${DATADIR}" FILE_PERMISSIONS OWNER_READ GROUP_READ WORLD_READ COMPONENT "Resources") + INSTALL(DIRECTORY "${OpenMW_BINARY_DIR}/resources" DESTINATION "${DATADIR}" COMPONENT "Resources") INSTALL(DIRECTORY DESTINATION "${DATADIR}/data" COMPONENT "Resources") ENDIF(NOT WIN32 AND NOT APPLE) @@ -446,25 +480,36 @@ if(WIN32) FILE(GLOB dll_files "${OpenMW_BINARY_DIR}/Release/*.dll") INSTALL(FILES ${dll_files} DESTINATION ".") INSTALL(FILES "${OpenMW_BINARY_DIR}/openmw.cfg.install" DESTINATION "." RENAME "openmw.cfg") + INSTALL(FILES "${OpenMW_SOURCE_DIR}/CHANGELOG.md" DESTINATION "." RENAME "CHANGELOG.txt") + INSTALL(FILES "${OpenMW_SOURCE_DIR}/README.md" DESTINATION "." RENAME "README.txt") INSTALL(FILES - "${OpenMW_SOURCE_DIR}/readme.txt" "${OpenMW_SOURCE_DIR}/Docs/license/GPL3.txt" "${OpenMW_SOURCE_DIR}/Docs/license/DejaVu Font License.txt" "${OpenMW_BINARY_DIR}/settings-default.cfg" "${OpenMW_BINARY_DIR}/transparency-overrides.cfg" + "${OpenMW_BINARY_DIR}/gamecontrollerdb.txt" "${OpenMW_BINARY_DIR}/Release/openmw.exe" DESTINATION ".") IF(BUILD_LAUNCHER) - INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/Release/omwlauncher.exe" DESTINATION ".") + INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/Release/openmw-launcher.exe" DESTINATION ".") ENDIF(BUILD_LAUNCHER) IF(BUILD_MWINIIMPORTER) - INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/Release/mwiniimport.exe" DESTINATION ".") + INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/Release/openmw-iniimporter.exe" DESTINATION ".") ENDIF(BUILD_MWINIIMPORTER) + IF(BUILD_ESSIMPORTER) + INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/Release/openmw-essimporter.exe" DESTINATION ".") + ENDIF(BUILD_ESSIMPORTER) IF(BUILD_OPENCS) - INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/Release/opencs.exe" DESTINATION ".") + INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/Release/openmw-cs.exe" DESTINATION ".") INSTALL(FILES "${OpenMW_BINARY_DIR}/opencs.ini" DESTINATION ".") ENDIF(BUILD_OPENCS) + IF(BUILD_WIZARD) + INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/Release/openmw-wizard.exe" DESTINATION ".") + ENDIF(BUILD_WIZARD) + if(BUILD_MYGUI_PLUGIN) + INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/Release/Plugin_MyGUI_OpenMW_Resources.dll" DESTINATION ".") + ENDIF(BUILD_MYGUI_PLUGIN) INSTALL(DIRECTORY "${OpenMW_BINARY_DIR}/resources" DESTINATION ".") @@ -477,25 +522,28 @@ if(WIN32) SET(CPACK_PACKAGE_VERSION_PATCH ${OPENMW_VERSION_RELEASE}) SET(CPACK_PACKAGE_EXECUTABLES "openmw;OpenMW") IF(BUILD_LAUNCHER) - SET(CPACK_PACKAGE_EXECUTABLES "${CPACK_PACKAGE_EXECUTABLES};omwlauncher;OpenMW Launcher") + SET(CPACK_PACKAGE_EXECUTABLES "${CPACK_PACKAGE_EXECUTABLES};openmw-launcher;OpenMW Launcher") ENDIF(BUILD_LAUNCHER) IF(BUILD_OPENCS) - SET(CPACK_PACKAGE_EXECUTABLES "${CPACK_PACKAGE_EXECUTABLES};opencs;OpenMW Construction Set") + SET(CPACK_PACKAGE_EXECUTABLES "${CPACK_PACKAGE_EXECUTABLES};openmw-cs;OpenMW Construction Set") ENDIF(BUILD_OPENCS) - SET(CPACK_NSIS_CREATE_ICONS_EXTRA "CreateShortCut '\$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\Readme.lnk' '\$INSTDIR\\\\readme.txt'") + IF(BUILD_WIZARD) + SET(CPACK_PACKAGE_EXECUTABLES "${CPACK_PACKAGE_EXECUTABLES};openmw-wizard;OpenMW Wizard") + ENDIF(BUILD_WIZARD) + SET(CPACK_NSIS_CREATE_ICONS_EXTRA "CreateShortCut '\$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\Readme.lnk' '\$INSTDIR\\\\README.txt'") SET(CPACK_NSIS_DELETE_ICONS_EXTRA " !insertmacro MUI_STARTMENU_GETFOLDER Application $MUI_TEMP Delete \\\"$SMPROGRAMS\\\\$MUI_TEMP\\\\Readme.lnk\\\" ") - SET(CPACK_RESOURCE_FILE_README "${OpenMW_SOURCE_DIR}/readme.txt") - SET(CPACK_PACKAGE_DESCRIPTION_FILE "${OpenMW_SOURCE_DIR}/readme.txt") + SET(CPACK_RESOURCE_FILE_README "${OpenMW_SOURCE_DIR}/README.md") + SET(CPACK_PACKAGE_DESCRIPTION_FILE "${OpenMW_SOURCE_DIR}/README.md") SET(CPACK_NSIS_EXECUTABLES_DIRECTORY ".") SET(CPACK_NSIS_DISPLAY_NAME "OpenMW ${OPENMW_VERSION}") SET(CPACK_NSIS_HELP_LINK "http:\\\\\\\\www.openmw.org") SET(CPACK_NSIS_URL_INFO_ABOUT "http:\\\\\\\\www.openmw.org") - SET(CPACK_NSIS_INSTALLED_ICON_NAME "omwlauncher.exe") - SET(CPACK_NSIS_MUI_ICON "${OpenMW_SOURCE_DIR}/files/launcher/images/openmw.ico") - SET(CPACK_NSIS_MUI_UNIICON "${OpenMW_SOURCE_DIR}/files/launcher/images/openmw.ico") + SET(CPACK_NSIS_INSTALLED_ICON_NAME "openmw-launcher.exe") + SET(CPACK_NSIS_MUI_ICON "${OpenMW_SOURCE_DIR}/files/windows/openmw.ico") + SET(CPACK_NSIS_MUI_UNIICON "${OpenMW_SOURCE_DIR}/files/windows/openmw.ico") SET(CPACK_PACKAGE_ICON "${OpenMW_SOURCE_DIR}\\\\files\\\\openmw.bmp") SET(VCREDIST32 "${OpenMW_BINARY_DIR}/vcredist_x86.exe") @@ -526,12 +574,23 @@ endif(WIN32) # Extern add_subdirectory (extern/shiny) +add_subdirectory (extern/ogre-ffmpeg-videoplayer) add_subdirectory (extern/oics) add_subdirectory (extern/sdl4ogre) # Components add_subdirectory (components) +# Plugins +if (BUILD_MYGUI_PLUGIN) + add_subdirectory(plugins/mygui_resource_plugin) +endif() + +#Testing +if (BUILD_NIFTEST) + add_subdirectory(components/nif/tests/) +endif(BUILD_NIFTEST) + # Apps and tools add_subdirectory( apps/openmw ) @@ -544,13 +603,6 @@ if (BUILD_ESMTOOL) endif() if (BUILD_LAUNCHER) - if(NOT WIN32) - find_package(LIBUNSHIELD REQUIRED) - if(NOT LIBUNSHIELD_FOUND) - message(SEND_ERROR "Failed to find libunshield") - endif(NOT LIBUNSHIELD_FOUND) - endif(NOT WIN32) - add_subdirectory( apps/launcher ) endif() @@ -558,10 +610,18 @@ if (BUILD_MWINIIMPORTER) add_subdirectory( apps/mwiniimporter ) endif() +if (BUILD_ESSIMPORTER) + add_subdirectory (apps/essimporter ) +endif() + if (BUILD_OPENCS) add_subdirectory (apps/opencs) endif() +if (BUILD_WIZARD) + add_subdirectory(apps/wizard) +endif() + # UnitTests if (BUILD_UNITTESTS) add_subdirectory( apps/openmw_test_suite ) @@ -569,6 +629,16 @@ endif() if (WIN32) if (MSVC) + if (MULTITHREADED_BUILD) + set( MT_BUILD "/MP") + endif (MULTITHREADED_BUILD) + + foreach( OUTPUTCONFIG ${CMAKE_CONFIGURATION_TYPES} ) + string( TOUPPER ${OUTPUTCONFIG} OUTPUTCONFIG ) + set( CMAKE_RUNTIME_OUTPUT_DIRECTORY_${OUTPUTCONFIG} "$(SolutionDir)$(Configuration)" ) + set( CMAKE_LIBRARY_OUTPUT_DIRECTORY_${OUTPUTCONFIG} "$(ProjectDir)$(Configuration)" ) + endforeach( OUTPUTCONFIG ) + if (USE_DEBUG_CONSOLE) set_target_properties(openmw PROPERTIES LINK_FLAGS_DEBUG "/SUBSYSTEM:CONSOLE") set_target_properties(openmw PROPERTIES LINK_FLAGS_RELWITHDEBINFO "/SUBSYSTEM:CONSOLE") @@ -608,10 +678,15 @@ if (WIN32) 4193 # #pragma warning(pop) : no matching '#pragma warning(push)' 4251 # class 'XXXX' needs to have dll-interface to be used by clients of class 'YYYY' 4275 # non dll-interface struct 'XXXX' used as base for dll-interface class 'YYYY' + 4315 # undocumented, 'this' pointer for member might not be aligned (OgreMemoryStlAllocator.h) + + # caused by boost + 4191 # 'type cast' : unsafe conversion (1.56, thread_primitives.hpp, normally off) # OpenMW specific warnings 4099 # Type mismatch, declared class or struct is defined with other type 4100 # Unreferenced formal parameter (-Wunused-parameter) + 4101 # Unreferenced local variable (-Wunused-variable) 4127 # Conditional expression is constant 4242 # Storing value in a variable of a smaller type, possible loss of data 4244 # Storing value of one type in variable of another (size_t in int, for example) @@ -630,51 +705,23 @@ if (WIN32) set(WARNINGS "${WARNINGS} /wd${d}") endforeach(d) + set_property(GLOBAL APPEND_STRING PROPERTY COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") + # boost::wave has a few issues with signed / unsigned conversions, so we suppress those here set(SHINY_WARNINGS "${WARNINGS} /wd4245") - set_target_properties(shiny PROPERTIES COMPILE_FLAGS ${SHINY_WARNINGS}) - # there's an unreferenced local variable in the ogre platform, suppress it - set(SHINY_OGRE_WARNINGS "${WARNINGS} /wd4101") - set_target_properties(shiny.OgrePlatform PROPERTIES COMPILE_FLAGS ${SHINY_OGRE_WARNINGS}) - set_target_properties(sdl4ogre PROPERTIES COMPILE_FLAGS ${WARNINGS}) + set_target_properties(shiny PROPERTIES COMPILE_FLAGS "${SHINY_WARNINGS} ${MT_BUILD}") + # oics uses tinyxml, which has an initialized but unused variable set(OICS_WARNINGS "${WARNINGS} /wd4189") - set_target_properties(oics PROPERTIES COMPILE_FLAGS ${OICS_WARNINGS}) - set_target_properties(components PROPERTIES COMPILE_FLAGS ${WARNINGS}) - if (BUILD_LAUNCHER) - set_target_properties(omwlauncher PROPERTIES COMPILE_FLAGS ${WARNINGS}) - endif (BUILD_LAUNCHER) - set_target_properties(openmw PROPERTIES COMPILE_FLAGS ${WARNINGS}) - if (BUILD_BSATOOL) - set_target_properties(bsatool PROPERTIES COMPILE_FLAGS ${WARNINGS}) - endif (BUILD_BSATOOL) - if (BUILD_ESMTOOL) - set_target_properties(esmtool PROPERTIES COMPILE_FLAGS ${WARNINGS}) - endif (BUILD_ESMTOOL) + set_target_properties(oics PROPERTIES COMPILE_FLAGS "${OICS_WARNINGS} ${MT_BUILD}") + if (BUILD_OPENCS) - set_target_properties(opencs PROPERTIES COMPILE_FLAGS ${WARNINGS}) + # QT triggers an informational warning that the object layout may differ when compiled with /vd2 + set(OPENCS_WARNINGS "${WARNINGS} ${MT_BUILD} /wd4435") + set_target_properties(openmw-cs PROPERTIES COMPILE_FLAGS ${OPENCS_WARNINGS}) endif (BUILD_OPENCS) - if (BUILD_MWINIIMPORTER) - set_target_properties(mwiniimport PROPERTIES COMPILE_FLAGS ${WARNINGS}) - endif (BUILD_MWINIIMPORTER) endif(MSVC) - # Same for MinGW - if (MINGW) - if (USE_DEBUG_CONSOLE) - set_target_properties(openmw PROPERTIES LINK_FLAGS_DEBUG "-Wl,-subsystem,console") - set_target_properties(openmw PROPERTIES LINK_FLAGS_RELWITHDEBINFO "-Wl,-subsystem,console") - set_target_properties(openmw PROPERTIES COMPILE_DEFINITIONS_DEBUG "_CONSOLE") - else(USE_DEBUG_CONSOLE) - set_target_properties(openmw PROPERTIES LINK_FLAGS_DEBUG "-Wl,-subsystem,windows") - set_target_properties(openmw PROPERTIES LINK_FLAGS_RELWITHDEBINFO "-Wl,-subsystem,windows") - endif(USE_DEBUG_CONSOLE) - - set_target_properties(openmw PROPERTIES LINK_FLAGS_RELEASE "-Wl,-subsystem,console") - set_target_properties(openmw PROPERTIES LINK_FLAGS_MINSIZEREL "-Wl,-subsystem,console") - set_target_properties(openmw PROPERTIES COMPILE_DEFINITIONS_RELEASE "_CONSOLE") - endif(MINGW) - # TODO: At some point release builds should not use the console but rather write to a log file #set_target_properties(openmw PROPERTIES LINK_FLAGS_RELEASE "/SUBSYSTEM:WINDOWS") #set_target_properties(openmw PROPERTIES LINK_FLAGS_MINSIZEREL "/SUBSYSTEM:WINDOWS") @@ -688,6 +735,7 @@ if (APPLE) install(DIRECTORY "${OpenMW_BINARY_DIR}/resources" DESTINATION "${INSTALL_SUBDIR}" COMPONENT Runtime) install(FILES "${OpenMW_BINARY_DIR}/openmw.cfg.install" RENAME "openmw.cfg" DESTINATION "${INSTALL_SUBDIR}" COMPONENT Runtime) install(FILES "${OpenMW_BINARY_DIR}/settings-default.cfg" DESTINATION "${INSTALL_SUBDIR}" COMPONENT Runtime) + install(FILES "${OpenMW_BINARY_DIR}/gamecontrollerdb.txt" DESTINATION "${INSTALL_SUBDIR}" COMPONENT Runtime) install(FILES "${OpenMW_BINARY_DIR}/transparency-overrides.cfg" DESTINATION "${INSTALL_SUBDIR}" COMPONENT Runtime) install(FILES "${OpenMW_BINARY_DIR}/opencs.ini" DESTINATION "${INSTALL_SUBDIR}" COMPONENT Runtime) @@ -699,7 +747,7 @@ if (APPLE) set(OPENMW_APP "\${CMAKE_INSTALL_PREFIX}/${INSTALL_SUBDIR}/${APP_BUNDLE_NAME}") - set(OPENCS_BUNDLE_NAME "OpenCS.app") + set(OPENCS_BUNDLE_NAME "OpenMW-CS.app") set(OPENCS_APP "\${CMAKE_INSTALL_PREFIX}/${INSTALL_SUBDIR}/${OPENCS_BUNDLE_NAME}") set(ABSOLUTE_PLUGINS "") @@ -789,3 +837,25 @@ if (APPLE) include(CPack) endif (APPLE) +# Doxygen Target -- simply run 'make doc' or 'make doc_pages' +# output directory for 'make doc' is "${OpenMW_BINARY_DIR}/docs/Doxygen" +# output directory for 'make doc_pages' is "${DOXYGEN_PAGES_OUTPUT_DIR}" if defined +# or "${OpenMW_BINARY_DIR}/docs/Pages" otherwise +find_package(Doxygen) +if (DOXYGEN_FOUND) + # determine output directory for doc_pages + if (NOT DEFINED DOXYGEN_PAGES_OUTPUT_DIR) + set(DOXYGEN_PAGES_OUTPUT_DIR "${OpenMW_BINARY_DIR}/docs/Pages") + endif () + configure_file(${OpenMW_SOURCE_DIR}/docs/Doxyfile.cmake ${OpenMW_BINARY_DIR}/docs/Doxyfile @ONLY) + configure_file(${OpenMW_SOURCE_DIR}/docs/DoxyfilePages.cmake ${OpenMW_BINARY_DIR}/docs/DoxyfilePages @ONLY) + add_custom_target(doc + ${DOXYGEN_EXECUTABLE} ${OpenMW_BINARY_DIR}/docs/Doxyfile + WORKING_DIRECTORY ${OpenMW_BINARY_DIR} + COMMENT "Generating Doxygen documentation at ${OpenMW_BINARY_DIR}/docs/Doxygen" + VERBATIM) + add_custom_target(doc_pages + ${DOXYGEN_EXECUTABLE} ${OpenMW_BINARY_DIR}/docs/DoxyfilePages + WORKING_DIRECTORY ${OpenMW_BINARY_DIR} + COMMENT "Generating documentation for the github-pages at ${DOXYGEN_PAGES_OUTPUT_DIR}" VERBATIM) +endif () diff --git a/README.md b/README.md new file mode 100644 index 000000000..63a313896 --- /dev/null +++ b/README.md @@ -0,0 +1,99 @@ +OpenMW +====== + +[![Build Status](https://img.shields.io/travis/OpenMW/openmw.svg?style=plastic)](https://travis-ci.org/OpenMW/openmw) [![Coverity Scan Build Status](https://scan.coverity.com/projects/3740/badge.svg)](https://scan.coverity.com/projects/3740) + +OpenMW is an attempt at recreating the engine for the popular role-playing game +Morrowind by Bethesda Softworks. You need to own and install the original game for OpenMW to work. + +* Version: 0.35.0 +* License: GPL (see docs/license/GPL3.txt for more information) +* Website: http://www.openmw.org +* IRC: #openmw on irc.freenode.net + +Font Licenses: +* DejaVuLGCSansMono.ttf: custom (see docs/license/DejaVu Font License.txt for more information) + +Getting Started +--------------- + +* [Official forums](https://forum.openmw.org/) +* [Installation instructions](https://wiki.openmw.org/index.php?title=Installation_Instructions) +* [Build from source](https://wiki.openmw.org/index.php?title=Development_Environment_Setup) +* [Testing the game](https://wiki.openmw.org/index.php?title=Testing) +* [How to contribute](https://wiki.openmw.org/index.php?title=Contribution_Wanted) +* [Report a bug](http://bugs.openmw.org/projects/openmw) - read the [guidelines](https://wiki.openmw.org/index.php?title=Bug_Reporting_Guidelines) before submitting your first bug! +* [Known issues] (http://bugs.openmw.org/projects/openmw/issues?utf8=%E2%9C%93&set_filter=1&f%5B%5D=status_id&op%5Bstatus_id%5D=%3D&v%5Bstatus_id%5D%5B%5D=7&f%5B%5D=tracker_id&op%5Btracker_id%5D=%3D&v%5Btracker_id%5D%5B%5D=1&f%5B%5D=&c%5B%5D=project&c%5B%5D=tracker&c%5B%5D=status&c%5B%5D=priority&c%5B%5D=subject&c%5B%5D=assigned_to&c%5B%5D=updated_on&group_by=tracker) + +The data path +------------- + +The data path tells OpenMW where to find your Morrowind files. If you run the launcher, OpenMW should be able to pick up the location of these files on its own, if both Morrowind and OpenMW are installed properly (installing Morrowind under WINE is considered a proper install). + +Command line options +-------------------- + + Syntax: openmw + Allowed options: + --help print help message + --version print version information and quit + --data arg (=data) set data directories (later directories + have higher priority) + --data-local arg set local data directory (highest + priority) + --fallback-archive arg (=fallback-archive) + set fallback BSA archives (later + archives have higher priority) + --resources arg (=resources) set resources directory + --start arg set initial cell + --content arg content file(s): esm/esp, or + omwgame/omwaddon + --no-sound [=arg(=1)] (=0) disable all sounds + --script-verbose [=arg(=1)] (=0) verbose script output + --script-all [=arg(=1)] (=0) compile all scripts (excluding dialogue + scripts) at startup + --script-all-dialogue [=arg(=1)] (=0) compile all dialogue scripts at startup + --script-console [=arg(=1)] (=0) enable console-only script + functionality + --script-run arg select a file containing a list of + console commands that is executed on + startup + --script-warn [=arg(=1)] (=1) handling of warnings when compiling + scripts + 0 - ignore warning + 1 - show warning but consider script as + correctly compiled anyway + 2 - treat warnings as errors + --script-blacklist arg ignore the specified script (if the use + of the blacklist is enabled) + --script-blacklist-use [=arg(=1)] (=1) + enable script blacklisting + --load-savegame arg load a save game file on game startup + (specify an absolute filename or a + filename relative to the current + working directory) + --skip-menu [=arg(=1)] (=0) skip main menu on game startup + --new-game [=arg(=1)] (=0) run new game sequence (ignored if + skip-menu=0) + --fs-strict [=arg(=1)] (=0) strict file system handling (no case + folding) + --encoding arg (=win1252) Character encoding used in OpenMW game + messages: + + win1250 - Central and Eastern European + such as Polish, Czech, Slovak, + Hungarian, Slovene, Bosnian, Croatian, + Serbian (Latin script), Romanian and + Albanian languages + + win1251 - Cyrillic alphabet such as + Russian, Bulgarian, Serbian Cyrillic + and other languages + + win1252 - Western European (Latin) + alphabet, used by default + --fallback arg fallback values + --no-grab Don't grab mouse cursor + --export-fonts [=arg(=1)] (=0) Export Morrowind .fnt fonts to PNG + image and XML file in current directory + --activate-dist arg (=-1) activation distance override diff --git a/apps/bsatool/bsatool.cpp b/apps/bsatool/bsatool.cpp index 3781dd066..c0a6dcc81 100644 --- a/apps/bsatool/bsatool.cpp +++ b/apps/bsatool/bsatool.cpp @@ -27,8 +27,8 @@ struct Arguments void replaceAll(std::string& str, const std::string& needle, const std::string& substitute) { - int pos = str.find(needle); - while(pos != -1) + size_t pos = str.find(needle); + while(pos != std::string::npos) { str.replace(pos, needle.size(), substitute); pos = str.find(needle); @@ -51,7 +51,7 @@ bool parseOptions (int argc, char** argv, Arguments &info) ("help,h", "print help message.") ("version,v", "print version information and quit.") ("long,l", "Include extra information in archive listing.") - ("full-path,f", "Create diretory hierarchy on file extraction " + ("full-path,f", "Create directory hierarchy on file extraction " "(always true for extractall).") ; @@ -138,8 +138,8 @@ bool parseOptions (int argc, char** argv, Arguments &info) else if (variables["input-file"].as< std::vector >().size() > 1) info.outdir = variables["input-file"].as< std::vector >()[1]; - info.longformat = variables.count("long"); - info.fullpath = variables.count("full-path"); + info.longformat = variables.count("long") != 0; + info.fullpath = variables.count("full-path") != 0; return true; } @@ -150,34 +150,33 @@ int extractAll(Bsa::BSAFile& bsa, Arguments& info); int main(int argc, char** argv) { - Arguments info; - if(!parseOptions (argc, argv, info)) - return 1; - - // Open file - Bsa::BSAFile bsa; try { + Arguments info; + if(!parseOptions (argc, argv, info)) + return 1; + + // Open file + Bsa::BSAFile bsa; bsa.open(info.filename); + + if (info.mode == "list") + return list(bsa, info); + else if (info.mode == "extract") + return extract(bsa, info); + else if (info.mode == "extractall") + return extractAll(bsa, info); + else + { + std::cout << "Unsupported mode. That is not supposed to happen." << std::endl; + return 1; + } } - catch(std::exception &e) + catch (std::exception& e) { - std::cout << "ERROR reading BSA archive '" << info.filename - << "'\nDetails:\n" << e.what() << std::endl; + std::cerr << "ERROR reading BSA archive\nDetails:\n" << e.what() << std::endl; return 2; } - - if (info.mode == "list") - return list(bsa, info); - else if (info.mode == "extract") - return extract(bsa, info); - else if (info.mode == "extractall") - return extractAll(bsa, info); - else - { - std::cout << "Unsupported mode. That is not supposed to happen." << std::endl; - return 1; - } } int list(Bsa::BSAFile& bsa, Arguments& info) @@ -189,9 +188,11 @@ int list(Bsa::BSAFile& bsa, Arguments& info) if(info.longformat) { // Long format + std::ios::fmtflags f(std::cout.flags()); std::cout << std::setw(50) << std::left << files[i].name; std::cout << std::setw(8) << std::left << std::dec << files[i].fileSize; std::cout << "@ 0x" << std::hex << files[i].offset << std::endl; + std::cout.flags(f); } else std::cout << files[i].name << std::endl; diff --git a/apps/esmtool/esmtool.cpp b/apps/esmtool/esmtool.cpp index ea908590a..98e18521e 100644 --- a/apps/esmtool/esmtool.cpp +++ b/apps/esmtool/esmtool.cpp @@ -22,7 +22,7 @@ struct ESMData { std::string author; std::string description; - int version; + unsigned int version; std::vector masters; std::deque mRecords; @@ -48,9 +48,9 @@ const std::set ESMData::sLabeledRec = // Based on the legacy struct struct Arguments { - unsigned int raw_given; - unsigned int quiet_given; - unsigned int loadcells_given; + bool raw_given; + bool quiet_given; + bool loadcells_given; bool plain_given; std::string mode; @@ -59,6 +59,7 @@ struct Arguments std::string outname; std::vector types; + std::string name; ESMData data; ESM::ESMReader reader; @@ -78,6 +79,8 @@ bool parseOptions (int argc, char** argv, Arguments &info) ("type,t", bpo::value< std::vector >(), "Show only records of this type (four character record code). May " "be specified multiple times. Only affects dump mode.") + ("name,n", bpo::value(), + "Show only the record with this name. Only affects dump mode.") ("plain,p", "Print contents of dialogs, books and scripts. " "(skipped by default)" "Only affects dump mode.") @@ -148,7 +151,9 @@ bool parseOptions (int argc, char** argv, Arguments &info) } if (variables.count("type") > 0) - info.types = variables["type"].as< std::vector >(); + info.types = variables["type"].as< std::vector >(); + if (variables.count("name") > 0) + info.name = variables["name"].as(); info.mode = variables["mode"].as(); if (!(info.mode == "dump" || info.mode == "clone" || info.mode == "comp")) @@ -177,10 +182,10 @@ bool parseOptions (int argc, char** argv, Arguments &info) if (variables["input-file"].as< std::vector >().size() > 1) info.outname = variables["input-file"].as< std::vector >()[1]; - info.raw_given = variables.count ("raw"); - info.quiet_given = variables.count ("quiet"); - info.loadcells_given = variables.count ("loadcells"); - info.plain_given = (variables.count("plain") > 0); + info.raw_given = variables.count ("raw") != 0; + info.quiet_given = variables.count ("quiet") != 0; + info.loadcells_given = variables.count ("loadcells") != 0; + info.plain_given = variables.count("plain") != 0; // Font encoding settings info.encoding = variables["encoding"].as(); @@ -203,19 +208,27 @@ int comp(Arguments& info); int main(int argc, char**argv) { - Arguments info; - if(!parseOptions (argc, argv, info)) - return 1; - - if (info.mode == "dump") - return load(info); - else if (info.mode == "clone") - return clone(info); - else if (info.mode == "comp") - return comp(info); - else + try + { + Arguments info; + if(!parseOptions (argc, argv, info)) + return 1; + + if (info.mode == "dump") + return load(info); + else if (info.mode == "clone") + return clone(info); + else if (info.mode == "comp") + return comp(info); + else + { + std::cout << "Invalid or no mode specified, dying horribly. Have a nice day." << std::endl; + return 1; + } + } + catch (std::exception& e) { - std::cout << "Invalid or no mode specified, dying horribly. Have a nice day." << std::endl; + std::cerr << "ERROR: " << e.what() << std::endl; return 1; } @@ -253,10 +266,12 @@ void loadCell(ESM::Cell &cell, ESM::ESMReader &esm, Arguments& info) std::cout << " Faction: '" << ref.mFaction << "'" << std::endl; std::cout << " Faction rank: '" << ref.mFactionRank << "'" << std::endl; std::cout << " Enchantment charge: '" << ref.mEnchantmentCharge << "'\n"; - std::cout << " Uses/health: '" << ref.mCharge << "'\n"; + std::cout << " Uses/health: '" << ref.mChargeInt << "'\n"; std::cout << " Gold value: '" << ref.mGoldValue << "'\n"; std::cout << " Blocked: '" << static_cast(ref.mReferenceBlocked) << "'" << std::endl; std::cout << " Deleted: " << deleted << std::endl; + if (!ref.mKey.empty()) + std::cout << " Key: '" << ref.mKey << "'" << std::endl; } } @@ -273,8 +288,10 @@ void printRaw(ESM::ESMReader &esm) esm.getSubName(); esm.skipHSub(); n = esm.retSubName(); + std::ios::fmtflags f(std::cout.flags()); std::cout << " " << n.toString() << " - " << esm.getSubSize() << " bytes @ 0x" << std::hex << offs << "\n"; + std::cout.flags(f); } } } @@ -348,6 +365,9 @@ int load(Arguments& info) if (id.empty()) id = esm.getHNOString("INAM"); + if (!info.name.empty() && !Misc::StringUtils::ciEqual(info.name, id)) + interested = false; + if(!quiet && interested) std::cout << "\nRecord: " << n.toString() << " '" << id << "'\n"; @@ -375,7 +395,7 @@ int load(Arguments& info) record->load(esm); if (!quiet && interested) record->print(); - if (record->getType().val == ESM::REC_CELL && loadCells) { + if (record->getType().val == ESM::REC_CELL && loadCells && interested) { loadCell(record->cast()->get(), esm, info); } @@ -420,7 +440,7 @@ int clone(Arguments& info) return 1; } - int recordCount = info.data.mRecords.size(); + size_t recordCount = info.data.mRecords.size(); int digitCount = 1; // For a nicer output if (recordCount > 9) ++digitCount; @@ -491,9 +511,9 @@ int clone(Arguments& info) if (!info.data.mCellRefs[ptr].empty()) { typedef std::deque RefList; RefList &refs = info.data.mCellRefs[ptr]; - for (RefList::iterator it = refs.begin(); it != refs.end(); ++it) + for (RefList::iterator refIt = refs.begin(); refIt != refs.end(); ++refIt) { - it->save(esm); + refIt->save(esm); } } } @@ -501,7 +521,7 @@ int clone(Arguments& info) esm.endRecord(name.toString()); saved++; - int perc = (saved / (float)recordCount)*100; + int perc = (int)((saved / (float)recordCount)*100); if (perc % 10 == 0) { std::cerr << "\r" << perc << "%"; diff --git a/apps/esmtool/labels.cpp b/apps/esmtool/labels.cpp index ef45989ef..88e188df0 100644 --- a/apps/esmtool/labels.cpp +++ b/apps/esmtool/labels.cpp @@ -13,142 +13,147 @@ #include #include -#include #include std::string bodyPartLabel(int idx) { - const char *bodyPartLabels[] = { - "Head", - "Hair", - "Neck", - "Cuirass", - "Groin", - "Skirt", - "Right Hand", - "Left Hand", - "Right Wrist", - "Left Wrist", - "Shield", - "Right Forearm", - "Left Forearm", - "Right Upperarm", - "Left Upperarm", - "Right Foot", - "Left Foot", - "Right Ankle", - "Left Ankle", - "Right Knee", - "Left Knee", - "Right Leg", - "Left Leg", - "Right Shoulder", - "Left Shoulder", - "Weapon", - "Tail" - }; - if (idx >= 0 && idx <= 26) + { + static const char *bodyPartLabels[] = { + "Head", + "Hair", + "Neck", + "Cuirass", + "Groin", + "Skirt", + "Right Hand", + "Left Hand", + "Right Wrist", + "Left Wrist", + "Shield", + "Right Forearm", + "Left Forearm", + "Right Upperarm", + "Left Upperarm", + "Right Foot", + "Left Foot", + "Right Ankle", + "Left Ankle", + "Right Knee", + "Left Knee", + "Right Leg", + "Left Leg", + "Right Shoulder", + "Left Shoulder", + "Weapon", + "Tail" + }; return bodyPartLabels[idx]; + } else return "Invalid"; } std::string meshPartLabel(int idx) { - const char *meshPartLabels[] = { - "Head", - "Hair", - "Neck", - "Chest", - "Groin", - "Hand", - "Wrist", - "Forearm", - "Upperarm", - "Foot", - "Ankle", - "Knee", - "Upper Leg", - "Clavicle", - "Tail" - }; - if (idx >= 0 && idx <= ESM::BodyPart::MP_Tail) + { + static const char *meshPartLabels[] = { + "Head", + "Hair", + "Neck", + "Chest", + "Groin", + "Hand", + "Wrist", + "Forearm", + "Upperarm", + "Foot", + "Ankle", + "Knee", + "Upper Leg", + "Clavicle", + "Tail" + }; return meshPartLabels[idx]; + } else return "Invalid"; } std::string meshTypeLabel(int idx) { - const char *meshTypeLabels[] = { - "Skin", - "Clothing", - "Armor" - }; - if (idx >= 0 && idx <= ESM::BodyPart::MT_Armor) + { + static const char *meshTypeLabels[] = { + "Skin", + "Clothing", + "Armor" + }; return meshTypeLabels[idx]; + } else return "Invalid"; } std::string clothingTypeLabel(int idx) { - const char *clothingTypeLabels[] = { - "Pants", - "Shoes", - "Shirt", - "Belt", - "Robe", - "Right Glove", - "Left Glove", - "Skirt", - "Ring", - "Amulet" - }; - if (idx >= 0 && idx <= 9) + { + static const char *clothingTypeLabels[] = { + "Pants", + "Shoes", + "Shirt", + "Belt", + "Robe", + "Right Glove", + "Left Glove", + "Skirt", + "Ring", + "Amulet" + }; return clothingTypeLabels[idx]; + } else return "Invalid"; } std::string armorTypeLabel(int idx) -{ - const char *armorTypeLabels[] = { - "Helmet", - "Cuirass", - "Left Pauldron", - "Right Pauldron", - "Greaves", - "Boots", - "Left Gauntlet", - "Right Gauntlet", - "Shield", - "Left Bracer", - "Right Bracer" - }; - +{ if (idx >= 0 && idx <= 10) + { + static const char *armorTypeLabels[] = { + "Helmet", + "Cuirass", + "Left Pauldron", + "Right Pauldron", + "Greaves", + "Boots", + "Left Gauntlet", + "Right Gauntlet", + "Shield", + "Left Bracer", + "Right Bracer" + }; return armorTypeLabels[idx]; + } else return "Invalid"; } std::string dialogTypeLabel(int idx) { - const char *dialogTypeLabels[] = { - "Topic", - "Voice", - "Greeting", - "Persuasion", - "Journal" - }; - if (idx >= 0 && idx <= 4) + { + static const char *dialogTypeLabels[] = { + "Topic", + "Voice", + "Greeting", + "Persuasion", + "Journal" + }; return dialogTypeLabels[idx]; + } else if (idx == -1) return "Deleted"; else @@ -157,75 +162,79 @@ std::string dialogTypeLabel(int idx) std::string questStatusLabel(int idx) { - const char *questStatusLabels[] = { - "None", - "Name", - "Finished", - "Restart", - "Deleted" - }; - if (idx >= 0 && idx <= 4) + { + static const char *questStatusLabels[] = { + "None", + "Name", + "Finished", + "Restart", + "Deleted" + }; return questStatusLabels[idx]; + } else return "Invalid"; } std::string creatureTypeLabel(int idx) { - const char *creatureTypeLabels[] = { - "Creature", - "Daedra", - "Undead", - "Humanoid", - }; - if (idx >= 0 && idx <= 3) + { + static const char *creatureTypeLabels[] = { + "Creature", + "Daedra", + "Undead", + "Humanoid", + }; return creatureTypeLabels[idx]; + } else return "Invalid"; } std::string soundTypeLabel(int idx) { - const char *soundTypeLabels[] = { - "Left Foot", - "Right Foot", - "Swim Left", - "Swim Right", - "Moan", - "Roar", - "Scream", - "Land" - }; - if (idx >= 0 && idx <= 7) + { + static const char *soundTypeLabels[] = { + "Left Foot", + "Right Foot", + "Swim Left", + "Swim Right", + "Moan", + "Roar", + "Scream", + "Land" + }; return soundTypeLabels[idx]; + } else return "Invalid"; } std::string weaponTypeLabel(int idx) { - const char *weaponTypeLabels[] = { - "Short Blade One Hand", - "Long Blade One Hand", - "Long Blade Two Hand", - "Blunt One Hand", - "Blunt Two Close", - "Blunt Two Wide", - "Spear Two Wide", - "Axe One Hand", - "Axe Two Hand", - "Marksman Bow", - "Marksman Crossbow", - "Marksman Thrown", - "Arrow", - "Bolt" - }; - if (idx >= 0 && idx <= 13) + { + static const char *weaponTypeLabels[] = { + "Short Blade One Hand", + "Long Blade One Hand", + "Long Blade Two Hand", + "Blunt One Hand", + "Blunt Two Close", + "Blunt Two Wide", + "Spear Two Wide", + "Axe One Hand", + "Axe Two Hand", + "Marksman Bow", + "Marksman Crossbow", + "Marksman Thrown", + "Arrow", + "Bolt" + }; return weaponTypeLabels[idx]; + } else return "Invalid"; } @@ -242,377 +251,397 @@ std::string aiTypeLabel(int type) std::string magicEffectLabel(int idx) { - const char* magicEffectLabels [] = { - "Water Breathing", - "Swift Swim", - "Water Walking", - "Shield", - "Fire Shield", - "Lightning Shield", - "Frost Shield", - "Burden", - "Feather", - "Jump", - "Levitate", - "SlowFall", - "Lock", - "Open", - "Fire Damage", - "Shock Damage", - "Frost Damage", - "Drain Attribute", - "Drain Health", - "Drain Magicka", - "Drain Fatigue", - "Drain Skill", - "Damage Attribute", - "Damage Health", - "Damage Magicka", - "Damage Fatigue", - "Damage Skill", - "Poison", - "Weakness to Fire", - "Weakness to Frost", - "Weakness to Shock", - "Weakness to Magicka", - "Weakness to Common Disease", - "Weakness to Blight Disease", - "Weakness to Corprus Disease", - "Weakness to Poison", - "Weakness to Normal Weapons", - "Disintegrate Weapon", - "Disintegrate Armor", - "Invisibility", - "Chameleon", - "Light", - "Sanctuary", - "Night Eye", - "Charm", - "Paralyze", - "Silence", - "Blind", - "Sound", - "Calm Humanoid", - "Calm Creature", - "Frenzy Humanoid", - "Frenzy Creature", - "Demoralize Humanoid", - "Demoralize Creature", - "Rally Humanoid", - "Rally Creature", - "Dispel", - "Soultrap", - "Telekinesis", - "Mark", - "Recall", - "Divine Intervention", - "Almsivi Intervention", - "Detect Animal", - "Detect Enchantment", - "Detect Key", - "Spell Absorption", - "Reflect", - "Cure Common Disease", - "Cure Blight Disease", - "Cure Corprus Disease", - "Cure Poison", - "Cure Paralyzation", - "Restore Attribute", - "Restore Health", - "Restore Magicka", - "Restore Fatigue", - "Restore Skill", - "Fortify Attribute", - "Fortify Health", - "Fortify Magicka", - "Fortify Fatigue", - "Fortify Skill", - "Fortify Maximum Magicka", - "Absorb Attribute", - "Absorb Health", - "Absorb Magicka", - "Absorb Fatigue", - "Absorb Skill", - "Resist Fire", - "Resist Frost", - "Resist Shock", - "Resist Magicka", - "Resist Common Disease", - "Resist Blight Disease", - "Resist Corprus Disease", - "Resist Poison", - "Resist Normal Weapons", - "Resist Paralysis", - "Remove Curse", - "Turn Undead", - "Summon Scamp", - "Summon Clannfear", - "Summon Daedroth", - "Summon Dremora", - "Summon Ancestral Ghost", - "Summon Skeletal Minion", - "Summon Bonewalker", - "Summon Greater Bonewalker", - "Summon Bonelord", - "Summon Winged Twilight", - "Summon Hunger", - "Summon Golden Saint", - "Summon Flame Atronach", - "Summon Frost Atronach", - "Summon Storm Atronach", - "Fortify Attack", - "Command Creature", - "Command Humanoid", - "Bound Dagger", - "Bound Longsword", - "Bound Mace", - "Bound Battle Axe", - "Bound Spear", - "Bound Longbow", - "EXTRA SPELL", - "Bound Cuirass", - "Bound Helm", - "Bound Boots", - "Bound Shield", - "Bound Gloves", - "Corprus", - "Vampirism", - "Summon Centurion Sphere", - "Sun Damage", - "Stunted Magicka", - "Summon Fabricant", - "sEffectSummonCreature01", - "sEffectSummonCreature02", - "sEffectSummonCreature03", - "sEffectSummonCreature04", - "sEffectSummonCreature05" - }; if (idx >= 0 && idx <= 142) + { + const char* magicEffectLabels [] = { + "Water Breathing", + "Swift Swim", + "Water Walking", + "Shield", + "Fire Shield", + "Lightning Shield", + "Frost Shield", + "Burden", + "Feather", + "Jump", + "Levitate", + "SlowFall", + "Lock", + "Open", + "Fire Damage", + "Shock Damage", + "Frost Damage", + "Drain Attribute", + "Drain Health", + "Drain Magicka", + "Drain Fatigue", + "Drain Skill", + "Damage Attribute", + "Damage Health", + "Damage Magicka", + "Damage Fatigue", + "Damage Skill", + "Poison", + "Weakness to Fire", + "Weakness to Frost", + "Weakness to Shock", + "Weakness to Magicka", + "Weakness to Common Disease", + "Weakness to Blight Disease", + "Weakness to Corprus Disease", + "Weakness to Poison", + "Weakness to Normal Weapons", + "Disintegrate Weapon", + "Disintegrate Armor", + "Invisibility", + "Chameleon", + "Light", + "Sanctuary", + "Night Eye", + "Charm", + "Paralyze", + "Silence", + "Blind", + "Sound", + "Calm Humanoid", + "Calm Creature", + "Frenzy Humanoid", + "Frenzy Creature", + "Demoralize Humanoid", + "Demoralize Creature", + "Rally Humanoid", + "Rally Creature", + "Dispel", + "Soultrap", + "Telekinesis", + "Mark", + "Recall", + "Divine Intervention", + "Almsivi Intervention", + "Detect Animal", + "Detect Enchantment", + "Detect Key", + "Spell Absorption", + "Reflect", + "Cure Common Disease", + "Cure Blight Disease", + "Cure Corprus Disease", + "Cure Poison", + "Cure Paralyzation", + "Restore Attribute", + "Restore Health", + "Restore Magicka", + "Restore Fatigue", + "Restore Skill", + "Fortify Attribute", + "Fortify Health", + "Fortify Magicka", + "Fortify Fatigue", + "Fortify Skill", + "Fortify Maximum Magicka", + "Absorb Attribute", + "Absorb Health", + "Absorb Magicka", + "Absorb Fatigue", + "Absorb Skill", + "Resist Fire", + "Resist Frost", + "Resist Shock", + "Resist Magicka", + "Resist Common Disease", + "Resist Blight Disease", + "Resist Corprus Disease", + "Resist Poison", + "Resist Normal Weapons", + "Resist Paralysis", + "Remove Curse", + "Turn Undead", + "Summon Scamp", + "Summon Clannfear", + "Summon Daedroth", + "Summon Dremora", + "Summon Ancestral Ghost", + "Summon Skeletal Minion", + "Summon Bonewalker", + "Summon Greater Bonewalker", + "Summon Bonelord", + "Summon Winged Twilight", + "Summon Hunger", + "Summon Golden Saint", + "Summon Flame Atronach", + "Summon Frost Atronach", + "Summon Storm Atronach", + "Fortify Attack", + "Command Creature", + "Command Humanoid", + "Bound Dagger", + "Bound Longsword", + "Bound Mace", + "Bound Battle Axe", + "Bound Spear", + "Bound Longbow", + "EXTRA SPELL", + "Bound Cuirass", + "Bound Helm", + "Bound Boots", + "Bound Shield", + "Bound Gloves", + "Corprus", + "Vampirism", + "Summon Centurion Sphere", + "Sun Damage", + "Stunted Magicka", + "Summon Fabricant", + "sEffectSummonCreature01", + "sEffectSummonCreature02", + "sEffectSummonCreature03", + "sEffectSummonCreature04", + "sEffectSummonCreature05" + }; return magicEffectLabels[idx]; + } else return "Invalid"; } std::string attributeLabel(int idx) { - const char* attributeLabels [] = { - "Strength", - "Intelligence", - "Willpower", - "Agility", - "Speed", - "Endurance", - "Personality", - "Luck" - }; if (idx >= 0 && idx <= 7) + { + const char* attributeLabels [] = { + "Strength", + "Intelligence", + "Willpower", + "Agility", + "Speed", + "Endurance", + "Personality", + "Luck" + }; return attributeLabels[idx]; + } else return "Invalid"; } std::string spellTypeLabel(int idx) { - const char* spellTypeLabels [] = { - "Spells", - "Abilities", - "Blight Disease", - "Disease", - "Curse", - "Powers" - }; if (idx >= 0 && idx <= 5) + { + const char* spellTypeLabels [] = { + "Spells", + "Abilities", + "Blight Disease", + "Disease", + "Curse", + "Powers" + }; return spellTypeLabels[idx]; + } else return "Invalid"; } std::string specializationLabel(int idx) { - const char* specializationLabels [] = { - "Combat", - "Magic", - "Stealth" - }; if (idx >= 0 && idx <= 2) + { + const char* specializationLabels [] = { + "Combat", + "Magic", + "Stealth" + }; return specializationLabels[idx]; + } else return "Invalid"; } std::string skillLabel(int idx) { - const char* skillLabels [] = { - "Block", - "Armorer", - "Medium Armor", - "Heavy Armor", - "Blunt Weapon", - "Long Blade", - "Axe", - "Spear", - "Athletics", - "Enchant", - "Destruction", - "Alteration", - "Illusion", - "Conjuration", - "Mysticism", - "Restoration", - "Alchemy", - "Unarmored", - "Security", - "Sneak", - "Acrobatics", - "Light Armor", - "Short Blade", - "Marksman", - "Mercantile", - "Speechcraft", - "Hand-to-hand" - }; if (idx >= 0 && idx <= 26) + { + const char* skillLabels [] = { + "Block", + "Armorer", + "Medium Armor", + "Heavy Armor", + "Blunt Weapon", + "Long Blade", + "Axe", + "Spear", + "Athletics", + "Enchant", + "Destruction", + "Alteration", + "Illusion", + "Conjuration", + "Mysticism", + "Restoration", + "Alchemy", + "Unarmored", + "Security", + "Sneak", + "Acrobatics", + "Light Armor", + "Short Blade", + "Marksman", + "Mercantile", + "Speechcraft", + "Hand-to-hand" + }; return skillLabels[idx]; + } else return "Invalid"; } std::string apparatusTypeLabel(int idx) { - const char* apparatusTypeLabels [] = { - "Mortar", - "Alembic", - "Calcinator", - "Retort", - }; if (idx >= 0 && idx <= 3) + { + const char* apparatusTypeLabels [] = { + "Mortar", + "Alembic", + "Calcinator", + "Retort", + }; return apparatusTypeLabels[idx]; + } else return "Invalid"; } std::string rangeTypeLabel(int idx) { - const char* rangeTypeLabels [] = { - "Self", - "Touch", - "Target" - }; if (idx >= 0 && idx <= 2) + { + const char* rangeTypeLabels [] = { + "Self", + "Touch", + "Target" + }; return rangeTypeLabels[idx]; + } else return "Invalid"; } std::string schoolLabel(int idx) { - const char* schoolLabels [] = { - "Alteration", - "Conjuration", - "Destruction", - "Illusion", - "Mysticism", - "Restoration" - }; if (idx >= 0 && idx <= 5) + { + const char* schoolLabels [] = { + "Alteration", + "Conjuration", + "Destruction", + "Illusion", + "Mysticism", + "Restoration" + }; return schoolLabels[idx]; + } else return "Invalid"; } std::string enchantTypeLabel(int idx) { - const char* enchantTypeLabels [] = { - "Cast Once", - "Cast When Strikes", - "Cast When Used", - "Constant Effect" - }; if (idx >= 0 && idx <= 3) + { + const char* enchantTypeLabels [] = { + "Cast Once", + "Cast When Strikes", + "Cast When Used", + "Constant Effect" + }; return enchantTypeLabels[idx]; + } else return "Invalid"; } std::string ruleFunction(int idx) { - std::string ruleFunctions[] = { - "Reaction Low", - "Reaction High", - "Rank Requirement", - "NPC? Reputation", - "Health Percent", - "Player Reputation", - "NPC Level", - "Player Health Percent", - "Player Magicka", - "Player Fatigue", - "Player Attribute Strength", - "Player Skill Block", - "Player Skill Armorer", - "Player Skill Medium Armor", - "Player Skill Heavy Armor", - "Player Skill Blunt Weapon", - "Player Skill Long Blade", - "Player Skill Axe", - "Player Skill Spear", - "Player Skill Athletics", - "Player Skill Enchant", - "Player Skill Destruction", - "Player Skill Alteration", - "Player Skill Illusion", - "Player Skill Conjuration", - "Player Skill Mysticism", - "Player SKill Restoration", - "Player Skill Alchemy", - "Player Skill Unarmored", - "Player Skill Security", - "Player Skill Sneak", - "Player Skill Acrobatics", - "Player Skill Light Armor", - "Player Skill Short Blade", - "Player Skill Marksman", - "Player Skill Mercantile", - "Player Skill Speechcraft", - "Player Skill Hand to Hand", - "Player Gender", - "Player Expelled from Faction", - "Player Diseased (Common)", - "Player Diseased (Blight)", - "Player Clothing Modifier", - "Player Crime Level", - "Player Same Sex", - "Player Same Race", - "Player Same Faction", - "Faction Rank Difference", - "Player Detected", - "Alarmed", - "Choice Selected", - "Player Attribute Intelligence", - "Player Attribute Willpower", - "Player Attribute Agility", - "Player Attribute Speed", - "Player Attribute Endurance", - "Player Attribute Personality", - "Player Attribute Luck", - "Player Diseased (Corprus)", - "Weather", - "Player is a Vampire", - "Player Level", - "Attacked", - "NPC Talked to Player", - "Player Health", - "Creature Target", - "Friend Hit", - "Fight", - "Hello", - "Alarm", - "Flee", - "Should Attack", - "Werewolf" - }; if (idx >= 0 && idx <= 72) + { + std::string ruleFunctions[] = { + "Reaction Low", + "Reaction High", + "Rank Requirement", + "NPC? Reputation", + "Health Percent", + "Player Reputation", + "NPC Level", + "Player Health Percent", + "Player Magicka", + "Player Fatigue", + "Player Attribute Strength", + "Player Skill Block", + "Player Skill Armorer", + "Player Skill Medium Armor", + "Player Skill Heavy Armor", + "Player Skill Blunt Weapon", + "Player Skill Long Blade", + "Player Skill Axe", + "Player Skill Spear", + "Player Skill Athletics", + "Player Skill Enchant", + "Player Skill Destruction", + "Player Skill Alteration", + "Player Skill Illusion", + "Player Skill Conjuration", + "Player Skill Mysticism", + "Player SKill Restoration", + "Player Skill Alchemy", + "Player Skill Unarmored", + "Player Skill Security", + "Player Skill Sneak", + "Player Skill Acrobatics", + "Player Skill Light Armor", + "Player Skill Short Blade", + "Player Skill Marksman", + "Player Skill Mercantile", + "Player Skill Speechcraft", + "Player Skill Hand to Hand", + "Player Gender", + "Player Expelled from Faction", + "Player Diseased (Common)", + "Player Diseased (Blight)", + "Player Clothing Modifier", + "Player Crime Level", + "Player Same Sex", + "Player Same Race", + "Player Same Faction", + "Faction Rank Difference", + "Player Detected", + "Alarmed", + "Choice Selected", + "Player Attribute Intelligence", + "Player Attribute Willpower", + "Player Attribute Agility", + "Player Attribute Speed", + "Player Attribute Endurance", + "Player Attribute Personality", + "Player Attribute Luck", + "Player Diseased (Corprus)", + "Weather", + "Player is a Vampire", + "Player Level", + "Attacked", + "NPC Talked to Player", + "Player Health", + "Creature Target", + "Friend Hit", + "Fight", + "Hello", + "Alarm", + "Flee", + "Should Attack", + "Werewolf" + }; return ruleFunctions[idx]; + } else return "Invalid"; } @@ -787,6 +816,10 @@ std::string magicEffectFlags(int flags) if (flags & ESM::MagicEffect::NonRecastable) properties += "NonRecastable "; if (flags & ESM::MagicEffect::Unreflectable) properties += "Unreflectable "; if (flags & ESM::MagicEffect::CasterLinked) properties += "CasterLinked "; + if (flags & ESM::MagicEffect::AllowSpellmaking) properties += "AllowSpellmaking "; + if (flags & ESM::MagicEffect::AllowEnchanting) properties += "AllowEnchanting "; + if (flags & ESM::MagicEffect::NegativeLight) properties += "NegativeLight "; + if (flags & 0xFFFC0000) properties += "Invalid "; properties += str(boost::format("(0x%08X)") % flags); return properties; diff --git a/apps/esmtool/record.cpp b/apps/esmtool/record.cpp index f8bc2af61..6fd4b80fb 100644 --- a/apps/esmtool/record.cpp +++ b/apps/esmtool/record.cpp @@ -2,6 +2,8 @@ #include "labels.hpp" #include +#include + #include void printAIPackage(ESM::AIPackage p) @@ -25,7 +27,7 @@ void printAIPackage(ESM::AIPackage p) { std::cout << " Travel Coordinates: (" << p.mTravel.mX << "," << p.mTravel.mY << "," << p.mTravel.mZ << ")" << std::endl; - std::cout << " Travel Unknown: " << (int)p.mTravel.mUnk << std::endl; + std::cout << " Travel Unknown: " << p.mTravel.mUnk << std::endl; } else if (p.mType == ESM::AI_Follow || p.mType == ESM::AI_Escort) { @@ -33,12 +35,12 @@ void printAIPackage(ESM::AIPackage p) << p.mTarget.mY << "," << p.mTarget.mZ << ")" << std::endl; std::cout << " Duration: " << p.mTarget.mDuration << std::endl; std::cout << " Target ID: " << p.mTarget.mId.toString() << std::endl; - std::cout << " Unknown: " << (int)p.mTarget.mUnk << std::endl; + std::cout << " Unknown: " << p.mTarget.mUnk << std::endl; } else if (p.mType == ESM::AI_Activate) { std::cout << " Name: " << p.mActivate.mName.toString() << std::endl; - std::cout << " Activate Unknown: " << (int)p.mActivate.mUnk << std::endl; + std::cout << " Activate Unknown: " << p.mActivate.mUnk << std::endl; } else { std::cout << " BadPackage: " << boost::format("0x%08x") % p.mType << std::endl; @@ -89,6 +91,7 @@ std::string ruleString(ESM::DialInfo::SelectStruct ss) case 'A': if (indicator == 'R') type_str = "Not Race"; break; case 'B': if (indicator == 'L') type_str = "Not Cell"; break; case 'C': if (indicator == 's') type_str = "Not Local"; break; + default: break; } // Append the variable name to the function string if any. @@ -110,6 +113,7 @@ std::string ruleString(ESM::DialInfo::SelectStruct ss) case '3': oper_str = ">="; break; case '4': oper_str = "< "; break; case '5': oper_str = "<="; break; + default: break; } std::ostringstream stream; @@ -412,7 +416,7 @@ void Record::print() std::cout << " Armor: " << mData.mData.mArmor << std::endl; std::cout << " Enchantment Points: " << mData.mData.mEnchant << std::endl; std::vector::iterator pit; - for (pit = mData.mParts.mParts.begin(); pit != mData.mParts.mParts.end(); pit++) + for (pit = mData.mParts.mParts.begin(); pit != mData.mParts.mParts.end(); ++pit) { std::cout << " Body Part: " << bodyPartLabel(pit->mPart) << " (" << (int)(pit->mPart) << ")" << std::endl; @@ -430,7 +434,7 @@ void Record::print() std::cout << " Icon: " << mData.mIcon << std::endl; std::cout << " Script: " << mData.mScript << std::endl; std::cout << " Type: " << apparatusTypeLabel(mData.mData.mType) - << " (" << (int)mData.mData.mType << ")" << std::endl; + << " (" << mData.mData.mType << ")" << std::endl; std::cout << " Weight: " << mData.mData.mWeight << std::endl; std::cout << " Value: " << mData.mData.mValue << std::endl; std::cout << " Quality: " << mData.mData.mQuality << std::endl; @@ -484,7 +488,7 @@ void Record::print() std::cout << " Texture: " << mData.mTexture << std::endl; std::cout << " Description: " << mData.mDescription << std::endl; std::vector::iterator pit; - for (pit = mData.mPowers.mList.begin(); pit != mData.mPowers.mList.end(); pit++) + for (pit = mData.mPowers.mList.begin(); pit != mData.mPowers.mList.end(); ++pit) std::cout << " Power: " << *pit << std::endl; } @@ -513,7 +517,7 @@ void Record::print() else std::cout << " Map Color: " << boost::format("0x%08X") % mData.mMapColor << std::endl; std::cout << " Water Level Int: " << mData.mWaterInt << std::endl; - std::cout << " RefId counter: " << mData.mRefIdCounter << std::endl; + std::cout << " RefId counter: " << mData.mRefNumCounter << std::endl; } @@ -531,10 +535,10 @@ void Record::print() std::cout << " Specialization: " << specializationLabel(mData.mData.mSpecialization) << " (" << mData.mData.mSpecialization << ")" << std::endl; for (int i = 0; i != 5; i++) - std::cout << " Major Skill: " << skillLabel(mData.mData.mSkills[i][0]) + std::cout << " Minor Skill: " << skillLabel(mData.mData.mSkills[i][0]) << " (" << mData.mData.mSkills[i][0] << ")" << std::endl; for (int i = 0; i != 5; i++) - std::cout << " Minor Skill: " << skillLabel(mData.mData.mSkills[i][1]) + std::cout << " Major Skill: " << skillLabel(mData.mData.mSkills[i][1]) << " (" << mData.mData.mSkills[i][1] << ")" << std::endl; } @@ -554,7 +558,7 @@ void Record::print() std::cout << " Value: " << mData.mData.mValue << std::endl; std::cout << " Enchantment Points: " << mData.mData.mEnchant << std::endl; std::vector::iterator pit; - for (pit = mData.mParts.mParts.begin(); pit != mData.mParts.mParts.end(); pit++) + for (pit = mData.mParts.mParts.begin(); pit != mData.mParts.mParts.end(); ++pit) { std::cout << " Body Part: " << bodyPartLabel(pit->mPart) << " (" << (int)(pit->mPart) << ")" << std::endl; @@ -574,7 +578,7 @@ void Record::print() std::cout << " Flags: " << containerFlags(mData.mFlags) << std::endl; std::cout << " Weight: " << mData.mWeight << std::endl; std::vector::iterator cit; - for (cit = mData.mInventory.mList.begin(); cit != mData.mInventory.mList.end(); cit++) + for (cit = mData.mInventory.mList.begin(); cit != mData.mInventory.mList.end(); ++cit) std::cout << " Inventory: Count: " << boost::format("%4d") % cit->mCount << " Item: " << cit->mItem.toString() << std::endl; } @@ -619,12 +623,12 @@ void Record::print() std::cout << " Gold: " << mData.mData.mGold << std::endl; std::vector::iterator cit; - for (cit = mData.mInventory.mList.begin(); cit != mData.mInventory.mList.end(); cit++) + for (cit = mData.mInventory.mList.begin(); cit != mData.mInventory.mList.end(); ++cit) std::cout << " Inventory: Count: " << boost::format("%4d") % cit->mCount << " Item: " << cit->mItem.toString() << std::endl; std::vector::iterator sit; - for (sit = mData.mSpells.mList.begin(); sit != mData.mSpells.mList.end(); sit++) + for (sit = mData.mSpells.mList.begin(); sit != mData.mSpells.mList.end(); ++sit) std::cout << " Spell: " << *sit << std::endl; std::cout << " Artifical Intelligence: " << mData.mHasAI << std::endl; @@ -639,7 +643,7 @@ void Record::print() std::cout << " AI Services:" << boost::format("0x%08X") % mData.mAiData.mServices << std::endl; std::vector::iterator pit; - for (pit = mData.mAiPackage.mList.begin(); pit != mData.mAiPackage.mList.end(); pit++) + for (pit = mData.mAiPackage.mList.begin(); pit != mData.mAiPackage.mList.end(); ++pit) printAIPackage(*pit); } @@ -706,7 +710,7 @@ void Record::print() << mData.mData.mRankData[i].mFactReaction << std::endl; } std::map::iterator rit; - for (rit = mData.mReactions.begin(); rit != mData.mReactions.end(); rit++) + for (rit = mData.mReactions.begin(); rit != mData.mReactions.end(); ++rit) std::cout << " Reaction: " << rit->second << " = " << rit->first << std::endl; } @@ -750,7 +754,7 @@ void Record::print() if (mData.mCell != "") std::cout << " Cell: " << mData.mCell << std::endl; if (mData.mData.mDisposition > 0) - std::cout << " Disposition: " << mData.mData.mDisposition << std::endl; + std::cout << " Disposition/Journal index: " << mData.mData.mDisposition << std::endl; if (mData.mData.mGender != ESM::DialInfo::NA) std::cout << " Gender: " << mData.mData.mGender << std::endl; if (mData.mSound != "") @@ -763,7 +767,7 @@ void Record::print() std::cout << " Unknown2: " << (int)mData.mData.mUnknown2 << std::endl; std::vector::iterator sit; - for (sit = mData.mSelects.begin(); sit != mData.mSelects.end(); sit++) + for (sit = mData.mSelects.begin(); sit != mData.mSelects.end(); ++sit) std::cout << " Select Rule: " << ruleString(*sit) << std::endl; if (mData.mResultScript != "") @@ -810,13 +814,12 @@ void Record::print() { std::cout << " Coordinates: (" << mData.mX << "," << mData.mY << ")" << std::endl; std::cout << " Flags: " << landFlags(mData.mFlags) << std::endl; - std::cout << " HasData: " << mData.mHasData << std::endl; std::cout << " DataTypes: " << mData.mDataTypes << std::endl; // Seems like this should done with reference counting in the // loader to me. But I'm not really knowledgable about this // record type yet. --Cory - bool wasLoaded = mData.mDataLoaded; + bool wasLoaded = (mData.mDataLoaded != 0); if (mData.mDataTypes) mData.loadData(mData.mDataTypes); if (mData.mDataLoaded) { @@ -834,8 +837,8 @@ void Record::print() std::cout << " Chance for None: " << (int)mData.mChanceNone << std::endl; std::cout << " Flags: " << creatureListFlags(mData.mFlags) << std::endl; std::cout << " Number of items: " << mData.mList.size() << std::endl; - std::vector::iterator iit; - for (iit = mData.mList.begin(); iit != mData.mList.end(); iit++) + std::vector::iterator iit; + for (iit = mData.mList.begin(); iit != mData.mList.end(); ++iit) std::cout << " Creature: Level: " << iit->mLevel << " Creature: " << iit->mId << std::endl; } @@ -846,8 +849,8 @@ void Record::print() std::cout << " Chance for None: " << (int)mData.mChanceNone << std::endl; std::cout << " Flags: " << itemListFlags(mData.mFlags) << std::endl; std::cout << " Number of items: " << mData.mList.size() << std::endl; - std::vector::iterator iit; - for (iit = mData.mList.begin(); iit != mData.mList.end(); iit++) + std::vector::iterator iit; + for (iit = mData.mList.begin(); iit != mData.mList.end(); ++iit) std::cout << " Inventory: Level: " << iit->mLevel << " Item: " << iit->mId << std::endl; } @@ -950,9 +953,9 @@ void Record::print() std::cout << " School: " << schoolLabel(mData.mData.mSchool) << " (" << mData.mData.mSchool << ")" << std::endl; std::cout << " Base Cost: " << mData.mData.mBaseCost << std::endl; + std::cout << " Unknown 1: " << mData.mData.mUnknown1 << std::endl; std::cout << " Speed: " << mData.mData.mSpeed << std::endl; - std::cout << " Size: " << mData.mData.mSize << std::endl; - std::cout << " Size Cap: " << mData.mData.mSizeCap << std::endl; + std::cout << " Unknown 2: " << mData.mData.mUnknown2 << std::endl; std::cout << " RGB Color: " << "(" << mData.mData.mRed << "," << mData.mData.mGreen << "," @@ -992,7 +995,6 @@ void Record::print() std::cout << " Level: " << mData.mNpdt12.mLevel << std::endl; std::cout << " Reputation: " << (int)mData.mNpdt12.mReputation << std::endl; std::cout << " Disposition: " << (int)mData.mNpdt12.mDisposition << std::endl; - std::cout << " Faction: " << (int)mData.mNpdt52.mFactionID << std::endl; std::cout << " Rank: " << (int)mData.mNpdt12.mRank << std::endl; std::cout << " Unknown1: " << (unsigned int)((unsigned char)mData.mNpdt12.mUnknown1) << std::endl; @@ -1000,13 +1002,14 @@ void Record::print() << (unsigned int)((unsigned char)mData.mNpdt12.mUnknown2) << std::endl; std::cout << " Unknown3: " << (unsigned int)((unsigned char)mData.mNpdt12.mUnknown3) << std::endl; - std::cout << " Gold: " << (int)mData.mNpdt12.mGold << std::endl; + std::cout << " Gold: " << mData.mNpdt12.mGold << std::endl; } else { std::cout << " Level: " << mData.mNpdt52.mLevel << std::endl; std::cout << " Reputation: " << (int)mData.mNpdt52.mReputation << std::endl; std::cout << " Disposition: " << (int)mData.mNpdt52.mDisposition << std::endl; std::cout << " Rank: " << (int)mData.mNpdt52.mRank << std::endl; + std::cout << " FactionID: " << (int)mData.mNpdt52.mFactionID << std::endl; std::cout << " Attributes:" << std::endl; std::cout << " Strength: " << (int)mData.mNpdt52.mStrength << std::endl; @@ -1021,7 +1024,7 @@ void Record::print() std::cout << " Skills:" << std::endl; for (int i = 0; i != ESM::Skill::Length; i++) std::cout << " " << skillLabel(i) << ": " - << (int)((unsigned char)mData.mNpdt52.mSkills[i]) << std::endl; + << (int)(mData.mNpdt52.mSkills[i]) << std::endl; std::cout << " Health: " << mData.mNpdt52.mHealth << std::endl; std::cout << " Magicka: " << mData.mNpdt52.mMana << std::endl; @@ -1031,16 +1034,16 @@ void Record::print() } std::vector::iterator cit; - for (cit = mData.mInventory.mList.begin(); cit != mData.mInventory.mList.end(); cit++) + for (cit = mData.mInventory.mList.begin(); cit != mData.mInventory.mList.end(); ++cit) std::cout << " Inventory: Count: " << boost::format("%4d") % cit->mCount << " Item: " << cit->mItem.toString() << std::endl; std::vector::iterator sit; - for (sit = mData.mSpells.mList.begin(); sit != mData.mSpells.mList.end(); sit++) + for (sit = mData.mSpells.mList.begin(); sit != mData.mSpells.mList.end(); ++sit) std::cout << " Spell: " << *sit << std::endl; std::vector::iterator dit; - for (dit = mData.mTransport.begin(); dit != mData.mTransport.end(); dit++) + for (dit = mData.mTransport.begin(); dit != mData.mTransport.end(); ++dit) { std::cout << " Destination Position: " << boost::format("%12.3f") % dit->mPos.pos[0] << "," @@ -1066,7 +1069,7 @@ void Record::print() std::cout << " AI Services:" << boost::format("0x%08X") % mData.mAiData.mServices << std::endl; std::vector::iterator pit; - for (pit = mData.mAiPackage.mList.begin(); pit != mData.mAiPackage.mList.end(); pit++) + for (pit = mData.mAiPackage.mList.begin(); pit != mData.mAiPackage.mList.end(); ++pit) printAIPackage(*pit); } @@ -1123,9 +1126,9 @@ void Record::print() std::cout << (male ? " Male:" : " Female:") << std::endl; - for (int i=0; i<8; ++i) - std::cout << " " << sAttributeNames[i] << ": " - << mData.mData.mAttributeValues[i].getValue (male) << std::endl; + for (int j=0; j<8; ++j) + std::cout << " " << sAttributeNames[j] << ": " + << mData.mData.mAttributeValues[j].getValue (male) << std::endl; std::cout << " Height: " << mData.mData.mHeight.getValue (male) << std::endl; std::cout << " Weight: " << mData.mData.mWeight.getValue (male) << std::endl; @@ -1140,7 +1143,7 @@ void Record::print() << mData.mData.mBonus[i].mBonus << std::endl; std::vector::iterator sit; - for (sit = mData.mPowers.mList.begin(); sit != mData.mPowers.mList.end(); sit++) + for (sit = mData.mPowers.mList.begin(); sit != mData.mPowers.mList.end(); ++sit) std::cout << " Power: " << *sit << std::endl; } @@ -1164,7 +1167,7 @@ void Record::print() if (mData.mSleepList != "") std::cout << " Sleep List: " << mData.mSleepList << std::endl; std::vector::iterator sit; - for (sit = mData.mSoundList.begin(); sit != mData.mSoundList.end(); sit++) + for (sit = mData.mSoundList.begin(); sit != mData.mSoundList.end(); ++sit) std::cout << " Sound: " << (int)sit->mChance << " = " << sit->mSound.toString() << std::endl; } @@ -1181,12 +1184,12 @@ void Record::print() std::vector::iterator vit; - for (vit = mData.mVarNames.begin(); vit != mData.mVarNames.end(); vit++) + for (vit = mData.mVarNames.begin(); vit != mData.mVarNames.end(); ++vit) std::cout << " Variable: " << *vit << std::endl; std::cout << " ByteCode: "; std::vector::iterator cit; - for (cit = mData.mScriptData.begin(); cit != mData.mScriptData.end(); cit++) + for (cit = mData.mScriptData.begin(); cit != mData.mScriptData.end(); ++cit) std::cout << boost::format("%02X") % (int)(*cit); std::cout << std::endl; @@ -1250,7 +1253,7 @@ void Record::print() template<> void Record::print() { - std::cout << "Start Script: " << mData.mScript << std::endl; + std::cout << "Start Script: " << mData.mId << std::endl; std::cout << "Start Data: " << mData.mData << std::endl; } diff --git a/apps/esmtool/record.hpp b/apps/esmtool/record.hpp index 45b6d0426..c1b90ac2b 100644 --- a/apps/esmtool/record.hpp +++ b/apps/esmtool/record.hpp @@ -19,7 +19,7 @@ namespace EsmTool { protected: std::string mId; - int mFlags; + uint32_t mFlags; ESM::NAME mType; bool mPrintPlain; @@ -40,11 +40,11 @@ namespace EsmTool mId = id; } - int getFlags() const { + uint32_t getFlags() const { return mFlags; } - void setFlags(int flags) { + void setFlags(uint32_t flags) { mFlags = flags; } @@ -52,10 +52,6 @@ namespace EsmTool return mType; } - bool getPrintPlain() const { - return mPrintPlain; - } - void setPrintPlain(bool plain) { mPrintPlain = plain; } diff --git a/apps/essimporter/CMakeLists.txt b/apps/essimporter/CMakeLists.txt new file mode 100644 index 000000000..72ef364ee --- /dev/null +++ b/apps/essimporter/CMakeLists.txt @@ -0,0 +1,43 @@ +set(ESSIMPORTER_FILES + main.cpp + importer.cpp + importplayer.cpp + importnpcc.cpp + importcrec.cpp + importcellref.cpp + importacdt.cpp + importinventory.cpp + importklst.cpp + importcntc.cpp + importgame.cpp + importinfo.cpp + importdial.cpp + importques.cpp + importjour.cpp + importscri.cpp + importscpt.cpp + importercontext.cpp + converter.cpp + convertacdt.cpp + convertnpcc.cpp + convertinventory.cpp + convertcrec.cpp + convertcntc.cpp + convertscri.cpp + convertscpt.cpp + convertplayer.cpp +) + +add_executable(openmw-essimporter + ${ESSIMPORTER_FILES} +) + +target_link_libraries(openmw-essimporter + ${Boost_LIBRARIES} + components +) + +if (BUILD_WITH_CODE_COVERAGE) + add_definitions (--coverage) + target_link_libraries(openmw-essimporter gcov) +endif() diff --git a/apps/essimporter/convertacdt.cpp b/apps/essimporter/convertacdt.cpp new file mode 100644 index 000000000..718403a8c --- /dev/null +++ b/apps/essimporter/convertacdt.cpp @@ -0,0 +1,52 @@ +#include "convertacdt.hpp" + +namespace ESSImport +{ + + int translateDynamicIndex(int mwIndex) + { + if (mwIndex == 1) + return 2; + else if (mwIndex == 2) + return 1; + return mwIndex; + } + + void convertACDT (const ACDT& acdt, ESM::CreatureStats& cStats) + { + for (int i=0; i<3; ++i) + { + int writeIndex = translateDynamicIndex(i); + cStats.mDynamic[writeIndex].mBase = acdt.mDynamic[i][1]; + cStats.mDynamic[writeIndex].mMod = acdt.mDynamic[i][1]; + cStats.mDynamic[writeIndex].mCurrent = acdt.mDynamic[i][0]; + } + for (int i=0; i<8; ++i) + { + cStats.mAttributes[i].mBase = acdt.mAttributes[i][1]; + cStats.mAttributes[i].mMod = acdt.mAttributes[i][0]; + cStats.mAttributes[i].mCurrent = acdt.mAttributes[i][0]; + } + cStats.mGoldPool = acdt.mGoldPool; + cStats.mTalkedTo = acdt.mFlags & TalkedToPlayer; + cStats.mAttacked = acdt.mFlags & Attacked; + } + + void convertACSC (const ACSC& acsc, ESM::CreatureStats& cStats) + { + cStats.mDead = acsc.mFlags & Dead; + } + + void convertNpcData (const ActorData& actorData, ESM::NpcStats& npcStats) + { + for (int i=0; i +#include +#include + +#include "importacdt.hpp" + +namespace ESSImport +{ + + // OpenMW uses Health,Magicka,Fatigue, MW uses Health,Fatigue,Magicka + int translateDynamicIndex(int mwIndex); + + + void convertACDT (const ACDT& acdt, ESM::CreatureStats& cStats); + void convertACSC (const ACSC& acsc, ESM::CreatureStats& cStats); + + void convertNpcData (const ActorData& actorData, ESM::NpcStats& npcStats); +} + +#endif diff --git a/apps/essimporter/convertcntc.cpp b/apps/essimporter/convertcntc.cpp new file mode 100644 index 000000000..426ef4496 --- /dev/null +++ b/apps/essimporter/convertcntc.cpp @@ -0,0 +1,13 @@ +#include "convertcntc.hpp" + +#include "convertinventory.hpp" + +namespace ESSImport +{ + + void convertCNTC(const CNTC &cntc, ESM::ContainerState &state) + { + convertInventory(cntc.mInventory, state.mInventory); + } + +} diff --git a/apps/essimporter/convertcntc.hpp b/apps/essimporter/convertcntc.hpp new file mode 100644 index 000000000..c299d87a1 --- /dev/null +++ b/apps/essimporter/convertcntc.hpp @@ -0,0 +1,15 @@ +#ifndef OPENMW_ESSIMPORT_CONVERTCNTC_H +#define OPENMW_ESSIMPORT_CONVERTCNTC_H + +#include "importcntc.hpp" + +#include + +namespace ESSImport +{ + + void convertCNTC(const CNTC& cntc, ESM::ContainerState& state); + +} + +#endif diff --git a/apps/essimporter/convertcrec.cpp b/apps/essimporter/convertcrec.cpp new file mode 100644 index 000000000..34e1c0070 --- /dev/null +++ b/apps/essimporter/convertcrec.cpp @@ -0,0 +1,13 @@ +#include "convertcrec.hpp" + +#include "convertinventory.hpp" + +namespace ESSImport +{ + + void convertCREC(const CREC &crec, ESM::CreatureState &state) + { + convertInventory(crec.mInventory, state.mInventory); + } + +} diff --git a/apps/essimporter/convertcrec.hpp b/apps/essimporter/convertcrec.hpp new file mode 100644 index 000000000..7d317f03e --- /dev/null +++ b/apps/essimporter/convertcrec.hpp @@ -0,0 +1,15 @@ +#ifndef OPENMW_ESSIMPORT_CONVERTCREC_H +#define OPENMW_ESSIMPORT_CONVERTCREC_H + +#include "importcrec.hpp" + +#include + +namespace ESSImport +{ + + void convertCREC(const CREC& crec, ESM::CreatureState& state); + +} + +#endif diff --git a/apps/essimporter/converter.cpp b/apps/essimporter/converter.cpp new file mode 100644 index 000000000..91d290f33 --- /dev/null +++ b/apps/essimporter/converter.cpp @@ -0,0 +1,389 @@ +#include "converter.hpp" + +#include + +#include +#include + +#include +#include + +#include "convertcrec.hpp" +#include "convertcntc.hpp" +#include "convertscri.hpp" + +namespace +{ + + void convertImage(char* data, int size, int width, int height, Ogre::PixelFormat pf, const std::string& out) + { + Ogre::Image screenshot; + Ogre::DataStreamPtr stream (new Ogre::MemoryDataStream(data, size)); + screenshot.loadRawData(stream, width, height, 1, pf); + screenshot.save(out); + } + + + void convertCellRef(const ESSImport::CellRef& cellref, ESM::ObjectState& objstate) + { + objstate.mEnabled = cellref.mEnabled; + objstate.mPosition = cellref.mPos; + objstate.mRef.mRefNum = cellref.mRefNum; + if (cellref.mDeleted) + objstate.mCount = 0; + convertSCRI(cellref.mSCRI, objstate.mLocals); + objstate.mHasLocals = !objstate.mLocals.mVariables.empty(); + } + + bool isIndexedRefId(const std::string& indexedRefId) + { + if (indexedRefId.size() <= 8) + return false; + + if (indexedRefId.find_first_not_of("0123456789") == std::string::npos) + return false; // entirely numeric refid, this is a reference to + // a dynamically created record e.g. player-enchanted weapon + + std::string index = indexedRefId.substr(indexedRefId.size()-8); + if(index.find_first_not_of("0123456789ABCDEF") == std::string::npos ) + return true; + return false; + } +} + +namespace ESSImport +{ + + + struct MAPH + { + unsigned int size; + unsigned int value; + }; + + void ConvertFMAP::read(ESM::ESMReader &esm) + { + MAPH maph; + esm.getHNT(maph, "MAPH"); + std::vector data; + esm.getSubNameIs("MAPD"); + esm.getSubHeader(); + data.resize(esm.getSubSize()); + esm.getExact(&data[0], data.size()); + + Ogre::DataStreamPtr stream (new Ogre::MemoryDataStream(&data[0], data.size())); + mGlobalMapImage.loadRawData(stream, maph.size, maph.size, 1, Ogre::PF_BYTE_RGB); + // to match openmw size + mGlobalMapImage.resize(maph.size*2, maph.size*2, Ogre::Image::FILTER_BILINEAR); + } + + void ConvertFMAP::write(ESM::ESMWriter &esm) + { + int numcells = mGlobalMapImage.getWidth() / 18; // NB truncating, doesn't divide perfectly + // with the 512x512 map the game has by default + int cellSize = mGlobalMapImage.getWidth()/numcells; + + // Note the upper left corner of the (0,0) cell should be at (width/2, height/2) + + mContext->mGlobalMapState.mBounds.mMinX = -numcells/2; + mContext->mGlobalMapState.mBounds.mMaxX = (numcells-1)/2; + mContext->mGlobalMapState.mBounds.mMinY = -(numcells-1)/2; + mContext->mGlobalMapState.mBounds.mMaxY = numcells/2; + + Ogre::Image image2; + std::vector data; + int width = cellSize*numcells; + int height = cellSize*numcells; + data.resize(width*height*4, 0); + image2.loadDynamicImage(&data[0], width, height, Ogre::PF_BYTE_RGBA); + + for (std::set >::const_iterator it = mContext->mExploredCells.begin(); it != mContext->mExploredCells.end(); ++it) + { + if (it->first > mContext->mGlobalMapState.mBounds.mMaxX + || it->first < mContext->mGlobalMapState.mBounds.mMinX + || it->second > mContext->mGlobalMapState.mBounds.mMaxY + || it->second < mContext->mGlobalMapState.mBounds.mMinY) + { + // out of bounds, I think this could happen, since the original engine had a fixed-size map + continue; + } + + int imageLeftSrc = mGlobalMapImage.getWidth()/2; + int imageTopSrc = mGlobalMapImage.getHeight()/2; + imageLeftSrc += it->first * cellSize; + imageTopSrc -= it->second * cellSize; + int imageLeftDst = width/2; + int imageTopDst = height/2; + imageLeftDst += it->first * cellSize; + imageTopDst -= it->second * cellSize; + for (int x=0; xmGlobalMapState.mImageData.resize(encoded->size()); + encoded->read(&mContext->mGlobalMapState.mImageData[0], encoded->size()); + + esm.startRecord(ESM::REC_GMAP); + mContext->mGlobalMapState.save(esm); + esm.endRecord(ESM::REC_GMAP); + } + + void ConvertCell::read(ESM::ESMReader &esm) + { + ESM::Cell cell; + std::string id = esm.getHNString("NAME"); + cell.mName = id; + cell.load(esm, false); + + // I wonder what 0x40 does? + if (cell.isExterior() && cell.mData.mFlags & 0x20) + { + mContext->mGlobalMapState.mMarkers.insert(std::make_pair(cell.mData.mX, cell.mData.mY)); + } + + // note if the player is in a nameless exterior cell, we will assign the cellId later based on player position + if (id == mContext->mPlayerCellName) + { + mContext->mPlayer.mCellId = cell.getCellId(); + } + + Cell newcell; + newcell.mCell = cell; + + // fog of war + // seems to be a 1-bit pixel format, 16*16 pixels + // TODO: add bleeding of FOW into neighbouring cells (openmw handles this by writing to the textures, + // MW handles it when rendering only) + unsigned char nam8[32]; + // exterior has 1 NAM8, interior can have multiple ones, and have an extra 4 byte flag at the start + // (probably offset of that specific fog texture?) + while (esm.isNextSub("NAM8")) + { + if (cell.isExterior()) // TODO: NAM8 occasionally exists for cells that haven't been explored. + // are there any flags marking explored cells? + mContext->mExploredCells.insert(std::make_pair(cell.mData.mX, cell.mData.mY)); + + esm.getSubHeader(); + + if (esm.getSubSize() == 36) + { + // flag on interiors + esm.skip(4); + } + + esm.getExact(nam8, 32); + + newcell.mFogOfWar.reserve(16*16); + for (int x=0; x<16; ++x) + { + for (int y=0; y<16; ++y) + { + size_t pos = x*16+y; + size_t bytepos = pos/8; + assert(bytepos<32); + int bit = pos%8; + newcell.mFogOfWar.push_back(((nam8[bytepos] >> bit) & (0x1)) ? 0xffffffff : 0x000000ff); + } + } + + if (cell.isExterior()) + { + std::ostringstream filename; + filename << "fog_" << cell.mData.mX << "_" << cell.mData.mY << ".tga"; + + convertImage((char*)&newcell.mFogOfWar[0], newcell.mFogOfWar.size()*4, 16, 16, Ogre::PF_BYTE_RGBA, filename.str()); + } + } + + // moved reference, not handled yet + // NOTE: MVRF can also occur in within normal references (importcellref.cpp)? + // this does not match the ESM file implementation, + // verify if that can happen with ESM files too + while (esm.isNextSub("MVRF")) + { + esm.skipHSub(); // skip MVRF + esm.getSubName(); + esm.skipHSub(); // skip CNDT + } + + std::vector cellrefs; + while (esm.hasMoreSubs() && esm.isNextSub("FRMR")) + { + CellRef ref; + ref.load (esm); + cellrefs.push_back(ref); + } + + while (esm.isNextSub("MPCD")) + { + float notepos[3]; + esm.getHT(notepos, 3*sizeof(float)); + + // Markers seem to be arranged in a 32*32 grid, notepos has grid-indices. + // This seems to be the reason markers can't be placed everywhere in interior cells, + // i.e. when the grid is exceeded. + // Converting the interior markers correctly could be rather tricky, but is probably similar logic + // as used for the FoW texture placement, which we need to figure out anyway + notepos[1] += 31.f; + notepos[0] += 0.5; + notepos[1] += 0.5; + notepos[0] = 8192 * notepos[0] / 32.f; + notepos[1] = 8192 * notepos[1] / 32.f; + if (cell.isExterior()) + { + notepos[0] += 8192 * cell.mData.mX; + notepos[1] += 8192 * cell.mData.mY; + } + // TODO: what encoding is this in? + std::string note = esm.getHNString("MPNT"); + ESM::CustomMarker marker; + marker.mWorldX = notepos[0]; + marker.mWorldY = notepos[1]; + marker.mNote = note; + marker.mCell = cell.getCellId(); + mMarkers.push_back(marker); + } + + newcell.mRefs = cellrefs; + + + if (cell.isExterior()) + mExtCells[std::make_pair(cell.mData.mX, cell.mData.mY)] = newcell; + else + mIntCells[id] = newcell; + } + + void ConvertCell::writeCell(const Cell &cell, ESM::ESMWriter& esm) + { + ESM::Cell esmcell = cell.mCell; + esm.startRecord(ESM::REC_CSTA); + ESM::CellState csta; + csta.mHasFogOfWar = 0; + csta.mId = esmcell.getCellId(); + csta.mId.save(esm); + // TODO csta.mLastRespawn; + // shouldn't be needed if we respawn on global schedule like in original MW + csta.mWaterLevel = esmcell.mWater; + csta.save(esm); + + for (std::vector::const_iterator refIt = cell.mRefs.begin(); refIt != cell.mRefs.end(); ++refIt) + { + const CellRef& cellref = *refIt; + ESM::CellRef out (cellref); + + // TODO: use mContext->mCreatures/mNpcs + + if (!isIndexedRefId(cellref.mIndexedRefId)) + { + // non-indexed RefNum, i.e. no CREC/NPCC/CNTC record associated with it + // this could be any type of object really (even creatures/npcs too) + out.mRefID = cellref.mIndexedRefId; + std::string idLower = Misc::StringUtils::lowerCase(out.mRefID); + + ESM::ObjectState objstate; + objstate.blank(); + objstate.mRef = out; + objstate.mRef.mRefID = idLower; + objstate.mHasCustomState = false; + convertCellRef(cellref, objstate); + esm.writeHNT ("OBJE", 0); + objstate.save(esm); + continue; + } + else + { + std::stringstream stream; + stream << std::hex << cellref.mIndexedRefId.substr(cellref.mIndexedRefId.size()-8,8); + int refIndex; + stream >> refIndex; + + out.mRefID = cellref.mIndexedRefId.substr(0,cellref.mIndexedRefId.size()-8); + std::string idLower = Misc::StringUtils::lowerCase(out.mRefID); + + std::map, NPCC>::const_iterator npccIt = mContext->mNpcChanges.find( + std::make_pair(refIndex, out.mRefID)); + if (npccIt != mContext->mNpcChanges.end()) + { + ESM::NpcState objstate; + objstate.blank(); + objstate.mRef = out; + objstate.mRef.mRefID = idLower; + // TODO: need more micromanagement here so we don't overwrite values + // from the ESM with default values + if (cellref.mHasACDT) + convertACDT(cellref.mACDT, objstate.mCreatureStats); + if (cellref.mHasACSC) + convertACSC(cellref.mACSC, objstate.mCreatureStats); + convertNpcData(cellref, objstate.mNpcStats); + convertNPCC(npccIt->second, objstate); + convertCellRef(cellref, objstate); + esm.writeHNT ("OBJE", ESM::REC_NPC_); + objstate.save(esm); + continue; + } + + std::map, CNTC>::const_iterator cntcIt = mContext->mContainerChanges.find( + std::make_pair(refIndex, out.mRefID)); + if (cntcIt != mContext->mContainerChanges.end()) + { + ESM::ContainerState objstate; + objstate.blank(); + objstate.mRef = out; + objstate.mRef.mRefID = idLower; + convertCNTC(cntcIt->second, objstate); + convertCellRef(cellref, objstate); + esm.writeHNT ("OBJE", ESM::REC_CONT); + objstate.save(esm); + continue; + } + + std::map, CREC>::const_iterator crecIt = mContext->mCreatureChanges.find( + std::make_pair(refIndex, out.mRefID)); + if (crecIt != mContext->mCreatureChanges.end()) + { + ESM::CreatureState objstate; + objstate.blank(); + objstate.mRef = out; + objstate.mRef.mRefID = idLower; + // TODO: need more micromanagement here so we don't overwrite values + // from the ESM with default values + if (cellref.mHasACDT) + convertACDT(cellref.mACDT, objstate.mCreatureStats); + if (cellref.mHasACSC) + convertACSC(cellref.mACSC, objstate.mCreatureStats); + convertCREC(crecIt->second, objstate); + convertCellRef(cellref, objstate); + esm.writeHNT ("OBJE", ESM::REC_CREA); + objstate.save(esm); + continue; + } + + std::stringstream error; + error << "Can't find type for " << cellref.mIndexedRefId << std::endl; + throw std::runtime_error(error.str()); + } + } + + esm.endRecord(ESM::REC_CSTA); + } + + void ConvertCell::write(ESM::ESMWriter &esm) + { + for (std::map::const_iterator it = mIntCells.begin(); it != mIntCells.end(); ++it) + writeCell(it->second, esm); + + for (std::map, Cell>::const_iterator it = mExtCells.begin(); it != mExtCells.end(); ++it) + writeCell(it->second, esm); + + for (std::vector::const_iterator it = mMarkers.begin(); it != mMarkers.end(); ++it) + { + esm.startRecord(ESM::REC_MARK); + it->save(esm); + esm.endRecord(ESM::REC_MARK); + } + } + +} diff --git a/apps/essimporter/converter.hpp b/apps/essimporter/converter.hpp new file mode 100644 index 000000000..c23083f8e --- /dev/null +++ b/apps/essimporter/converter.hpp @@ -0,0 +1,597 @@ +#ifndef OPENMW_ESSIMPORT_CONVERTER_H +#define OPENMW_ESSIMPORT_CONVERTER_H + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "importcrec.hpp" +#include "importcntc.hpp" + +#include "importercontext.hpp" +#include "importcellref.hpp" +#include "importklst.hpp" +#include "importgame.hpp" +#include "importinfo.hpp" +#include "importdial.hpp" +#include "importques.hpp" +#include "importjour.hpp" +#include "importscpt.hpp" + +#include "convertacdt.hpp" +#include "convertnpcc.hpp" +#include "convertscpt.hpp" +#include "convertplayer.hpp" + +namespace ESSImport +{ + +class Converter +{ +public: + /// @return the order for writing this converter's records to the output file, in relation to other converters + virtual int getStage() { return 1; } + + virtual ~Converter() {} + + void setContext(Context& context) { mContext = &context; } + + virtual void read(ESM::ESMReader& esm) + { + } + + /// Called after the input file has been read in completely, which may be necessary + /// if the conversion process relies on information in other records + virtual void write(ESM::ESMWriter& esm) + { + + } + +protected: + Context* mContext; +}; + +/// Default converter: simply reads the record and writes it unmodified to the output +template +class DefaultConverter : public Converter +{ +public: + virtual int getStage() { return 0; } + + virtual void read(ESM::ESMReader& esm) + { + std::string id = esm.getHNString("NAME"); + T record; + record.load(esm); + mRecords[id] = record; + } + + virtual void write(ESM::ESMWriter& esm) + { + for (typename std::map::const_iterator it = mRecords.begin(); it != mRecords.end(); ++it) + { + esm.startRecord(T::sRecordId); + esm.writeHNString("NAME", it->first); + it->second.save(esm); + esm.endRecord(T::sRecordId); + } + } + +protected: + std::map mRecords; +}; + +class ConvertNPC : public Converter +{ +public: + virtual void read(ESM::ESMReader &esm) + { + ESM::NPC npc; + std::string id = esm.getHNString("NAME"); + npc.load(esm); + if (id != "player") + { + // Handles changes to the NPC struct, but since there is no index here + // it will apply to ALL instances of the class. seems to be the reason for the + // "feature" in MW where changing AI settings of one guard will change it for all guards of that refID. + mContext->mNpcs[Misc::StringUtils::lowerCase(id)] = npc; + } + else + { + mContext->mPlayer.mObject.mCreatureStats.mLevel = npc.mNpdt52.mLevel; + mContext->mPlayerBase = npc; + std::map empty; + // FIXME: player start spells and birthsign spells aren't listed here, + // need to fix openmw to account for this + for (std::vector::const_iterator it = npc.mSpells.mList.begin(); it != npc.mSpells.mList.end(); ++it) + mContext->mPlayer.mObject.mCreatureStats.mSpells.mSpells[*it] = empty; + + // Clear the list now that we've written it, this prevents issues cropping up with + // ensureCustomData() in OpenMW tripping over no longer existing spells, where an error would be fatal. + mContext->mPlayerBase.mSpells.mList.clear(); + + // Same with inventory. Actually it's strange this would contain something, since there's already an + // inventory list in NPCC. There seems to be a fair amount of redundancy in this format. + mContext->mPlayerBase.mInventory.mList.clear(); + } + } +}; + +class ConvertCREA : public Converter +{ +public: + virtual void read(ESM::ESMReader &esm) + { + // See comment in ConvertNPC + ESM::Creature creature; + std::string id = esm.getHNString("NAME"); + creature.load(esm); + mContext->mCreatures[Misc::StringUtils::lowerCase(id)] = creature; + } +}; + +// Do we need ConvertCONT? +// I've seen a CONT record in a certain save file, but the container contents in it +// were identical to a corresponding CNTC record. See previous comment about redundancy... + +class ConvertGlobal : public DefaultConverter +{ +public: + virtual void read(ESM::ESMReader &esm) + { + std::string id = esm.getHNString("NAME"); + ESM::Global global; + global.load(esm); + if (Misc::StringUtils::ciEqual(id, "gamehour")) + mContext->mHour = global.mValue.getFloat(); + if (Misc::StringUtils::ciEqual(id, "day")) + mContext->mDay = global.mValue.getInteger(); + if (Misc::StringUtils::ciEqual(id, "month")) + mContext->mMonth = global.mValue.getInteger(); + if (Misc::StringUtils::ciEqual(id, "year")) + mContext->mYear = global.mValue.getInteger(); + mRecords[id] = global; + } +}; + +class ConvertClass : public DefaultConverter +{ +public: + virtual void read(ESM::ESMReader &esm) + { + std::string id = esm.getHNString("NAME"); + ESM::Class class_; + class_.load(esm); + + if (id == "NEWCLASSID_CHARGEN") + mContext->mCustomPlayerClassName = class_.mName; + + mRecords[id] = class_; + } +}; + +class ConvertBook : public DefaultConverter +{ +public: + virtual void read(ESM::ESMReader &esm) + { + std::string id = esm.getHNString("NAME"); + ESM::Book book; + book.load(esm); + if (book.mData.mSkillID == -1) + mContext->mPlayer.mObject.mNpcStats.mUsedIds.push_back(Misc::StringUtils::lowerCase(id)); + + mRecords[id] = book; + } +}; + +class ConvertNPCC : public Converter +{ +public: + virtual void read(ESM::ESMReader &esm) + { + std::string id = esm.getHNString("NAME"); + NPCC npcc; + npcc.load(esm); + if (id == "PlayerSaveGame") + { + convertNPCC(npcc, mContext->mPlayer.mObject); + } + else + { + int index = npcc.mNPDT.mIndex; + mContext->mNpcChanges.insert(std::make_pair(std::make_pair(index,id), npcc)); + } + } +}; + +class ConvertREFR : public Converter +{ +public: + virtual void read(ESM::ESMReader &esm) + { + REFR refr; + refr.load(esm); + assert(refr.mRefID == "PlayerSaveGame"); + mContext->mPlayer.mObject.mPosition = refr.mPos; + + ESM::CreatureStats& cStats = mContext->mPlayer.mObject.mCreatureStats; + convertACDT(refr.mActorData.mACDT, cStats); + + ESM::NpcStats& npcStats = mContext->mPlayer.mObject.mNpcStats; + convertNpcData(refr.mActorData, npcStats); + + mSelectedSpell = refr.mActorData.mSelectedSpell; + if (!refr.mActorData.mSelectedEnchantItem.empty()) + { + ESM::InventoryState& invState = mContext->mPlayer.mObject.mInventory; + + for (unsigned int i=0; imPlayer, mContext->mDialogueState.mKnownTopics, mFirstPersonCam); + } + virtual void write(ESM::ESMWriter &esm) + { + esm.startRecord(ESM::REC_CAM_); + esm.writeHNT("FIRS", mFirstPersonCam); + esm.endRecord(ESM::REC_CAM_); + } +private: + bool mFirstPersonCam; +}; + +class ConvertCNTC : public Converter +{ + virtual void read(ESM::ESMReader &esm) + { + std::string id = esm.getHNString("NAME"); + CNTC cntc; + cntc.load(esm); + mContext->mContainerChanges.insert(std::make_pair(std::make_pair(cntc.mIndex,id), cntc)); + } +}; + +class ConvertCREC : public Converter +{ +public: + virtual void read(ESM::ESMReader &esm) + { + std::string id = esm.getHNString("NAME"); + CREC crec; + crec.load(esm); + mContext->mCreatureChanges.insert(std::make_pair(std::make_pair(crec.mIndex,id), crec)); + } +}; + +class ConvertFMAP : public Converter +{ +public: + virtual void read(ESM::ESMReader &esm); + virtual void write(ESM::ESMWriter &esm); + +private: + Ogre::Image mGlobalMapImage; +}; + +class ConvertCell : public Converter +{ +public: + virtual void read(ESM::ESMReader& esm); + virtual void write(ESM::ESMWriter& esm); + +private: + struct Cell + { + ESM::Cell mCell; + std::vector mRefs; + std::vector mFogOfWar; + }; + + std::map mIntCells; + std::map, Cell> mExtCells; + + std::vector mMarkers; + + void writeCell(const Cell& cell, ESM::ESMWriter &esm); +}; + +class ConvertKLST : public Converter +{ +public: + virtual void read(ESM::ESMReader& esm) + { + KLST klst; + klst.load(esm); + mKillCounter = klst.mKillCounter; + + mContext->mPlayer.mObject.mNpcStats.mWerewolfKills = klst.mWerewolfKills; + } + + virtual void write(ESM::ESMWriter &esm) + { + esm.startRecord(ESM::REC_DCOU); + for (std::map::const_iterator it = mKillCounter.begin(); it != mKillCounter.end(); ++it) + { + esm.writeHNString("ID__", it->first); + esm.writeHNT ("COUN", it->second); + } + esm.endRecord(ESM::REC_DCOU); + } + +private: + std::map mKillCounter; +}; + +class ConvertFACT : public Converter +{ +public: + virtual void read(ESM::ESMReader& esm) + { + std::string id = esm.getHNString("NAME"); + ESM::Faction faction; + faction.load(esm); + + Misc::StringUtils::toLower(id); + for (std::map::const_iterator it = faction.mReactions.begin(); it != faction.mReactions.end(); ++it) + { + std::string faction2 = Misc::StringUtils::lowerCase(it->first); + mContext->mDialogueState.mChangedFactionReaction[id].insert(std::make_pair(faction2, it->second)); + } + } +}; + +/// Stolen items +class ConvertSTLN : public Converter +{ +public: + virtual void read(ESM::ESMReader &esm) + { + std::string itemid = esm.getHNString("NAME"); + Misc::StringUtils::toLower(itemid); + + while (esm.isNextSub("FNAM") || esm.isNextSub("ONAM")) + { + if (esm.retSubName().toString() == "FNAM") + { + std::string factionid = esm.getHString(); + mStolenItems[itemid].insert(std::make_pair(Misc::StringUtils::lowerCase(factionid), true)); + } + else + { + std::string ownerid = esm.getHString(); + mStolenItems[itemid].insert(std::make_pair(Misc::StringUtils::lowerCase(ownerid), false)); + } + } + } + virtual void write(ESM::ESMWriter &esm) + { + ESM::StolenItems items; + for (std::map >::const_iterator it = mStolenItems.begin(); it != mStolenItems.end(); ++it) + { + std::map, int> owners; + for (std::set::const_iterator ownerIt = it->second.begin(); ownerIt != it->second.end(); ++ownerIt) + { + owners.insert(std::make_pair(std::make_pair(ownerIt->first, ownerIt->second) + // Since OpenMW doesn't suffer from the owner contamination bug, + // it needs a count argument. But for legacy savegames, we don't know + // this count, so must assume all items of that ID are stolen, + // like vanilla MW did. + ,std::numeric_limits::max())); + } + + items.mStolenItems.insert(std::make_pair(it->first, owners)); + } + + esm.startRecord(ESM::REC_STLN); + items.write(esm); + esm.endRecord(ESM::REC_STLN); + } + +private: + typedef std::pair Owner; // + + std::map > mStolenItems; +}; + +/// Seen responses for a dialogue topic? +/// Each DIAL record is followed by a number of INFO records, I believe, just like in ESMs +/// Dialogue conversion problems: +/// - Journal is stored in one continuous HTML markup rather than each entry separately with associated info ID. +/// - Seen dialogue responses only store the INFO id, rather than the fulltext. +/// - Quest stages only store the INFO id, rather than the journal entry fulltext. +class ConvertINFO : public Converter +{ +public: + virtual void read(ESM::ESMReader& esm) + { + INFO info; + info.load(esm); + } +}; + +class ConvertDIAL : public Converter +{ +public: + virtual void read(ESM::ESMReader& esm) + { + std::string id = esm.getHNString("NAME"); + DIAL dial; + dial.load(esm); + if (dial.mIndex > 0) + mDials[id] = dial; + } + virtual void write(ESM::ESMWriter &esm) + { + for (std::map::const_iterator it = mDials.begin(); it != mDials.end(); ++it) + { + esm.startRecord(ESM::REC_QUES); + ESM::QuestState state; + state.mFinished = 0; + state.mState = it->second.mIndex; + state.mTopic = Misc::StringUtils::lowerCase(it->first); + state.save(esm); + esm.endRecord(ESM::REC_QUES); + } + } +private: + std::map mDials; +}; + +class ConvertQUES : public Converter +{ +public: + virtual void read(ESM::ESMReader& esm) + { + std::string id = esm.getHNString("NAME"); + QUES quest; + quest.load(esm); + } +}; + +class ConvertJOUR : public Converter +{ +public: + virtual void read(ESM::ESMReader& esm) + { + JOUR journal; + journal.load(esm); + } +}; + +class ConvertGAME : public Converter +{ +public: + ConvertGAME() : mHasGame(false) {} + + std::string toString(int weatherId) + { + switch (weatherId) + { + case 0: + return "clear"; + case 1: + return "cloudy"; + case 2: + return "foggy"; + case 3: + return "overcast"; + case 4: + return "rain"; + case 5: + return "thunderstorm"; + case 6: + return "ashstorm"; + case 7: + return "blight"; + case 8: + return "snow"; + case 9: + return "blizzard"; + case -1: + return ""; + default: + { + std::stringstream error; + error << "unknown weather id: " << weatherId; + throw std::runtime_error(error.str()); + } + } + } + + virtual void read(ESM::ESMReader &esm) + { + mGame.load(esm); + mHasGame = true; + } + + virtual void write(ESM::ESMWriter &esm) + { + if (!mHasGame) + return; + esm.startRecord(ESM::REC_WTHR); + ESM::WeatherState weather; + weather.mCurrentWeather = toString(mGame.mGMDT.mCurrentWeather); + weather.mNextWeather = toString(mGame.mGMDT.mNextWeather); + weather.mRemainingTransitionTime = mGame.mGMDT.mWeatherTransition/100.f*(0.015*24*3600); + weather.mHour = mContext->mHour; + weather.mWindSpeed = 0.f; + weather.mTimePassed = 0.0; + weather.mFirstUpdate = false; + weather.save(esm); + esm.endRecord(ESM::REC_WTHR); + } + +private: + bool mHasGame; + GAME mGame; +}; + +/// Running global script +class ConvertSCPT : public Converter +{ +public: + virtual void read(ESM::ESMReader &esm) + { + SCPT script; + script.load(esm); + ESM::GlobalScript out; + convertSCPT(script, out); + mScripts.push_back(out); + } + virtual void write(ESM::ESMWriter &esm) + { + for (std::vector::const_iterator it = mScripts.begin(); it != mScripts.end(); ++it) + { + esm.startRecord(ESM::REC_GSCR); + it->save(esm); + esm.endRecord(ESM::REC_GSCR); + } + } +private: + std::vector mScripts; +}; + +} + +#endif diff --git a/apps/essimporter/convertinventory.cpp b/apps/essimporter/convertinventory.cpp new file mode 100644 index 000000000..f476fe1ee --- /dev/null +++ b/apps/essimporter/convertinventory.cpp @@ -0,0 +1,30 @@ +#include "convertinventory.hpp" + +#include + +namespace ESSImport +{ + + void convertInventory(const Inventory &inventory, ESM::InventoryState &state) + { + int index = 0; + for (std::vector::const_iterator it = inventory.mItems.begin(); + it != inventory.mItems.end(); ++it) + { + ESM::ObjectState objstate; + objstate.blank(); + objstate.mRef = *it; + objstate.mRef.mRefID = Misc::StringUtils::lowerCase(it->mId); + objstate.mCount = std::abs(it->mCount); // restocking items have negative count in the savefile + // openmw handles them differently, so no need to set any flags + state.mItems.push_back(objstate); + if (it->mRelativeEquipmentSlot != -1) + // Note we should really write the absolute slot here, which we do not know about + // Not a big deal, OpenMW will auto-correct to a valid slot, the only problem is when + // an item could be equipped in two different slots (e.g. equipped two rings) + state.mEquipmentSlots[index] = it->mRelativeEquipmentSlot; + ++index; + } + } + +} diff --git a/apps/essimporter/convertinventory.hpp b/apps/essimporter/convertinventory.hpp new file mode 100644 index 000000000..8abe85a44 --- /dev/null +++ b/apps/essimporter/convertinventory.hpp @@ -0,0 +1,15 @@ +#ifndef OPENMW_ESSIMPORT_CONVERTINVENTORY_H +#define OPENMW_ESSIMPORT_CONVERTINVENTORY_H + +#include "importinventory.hpp" + +#include + +namespace ESSImport +{ + + void convertInventory (const Inventory& inventory, ESM::InventoryState& state); + +} + +#endif diff --git a/apps/essimporter/convertnpcc.cpp b/apps/essimporter/convertnpcc.cpp new file mode 100644 index 000000000..48d3d9232 --- /dev/null +++ b/apps/essimporter/convertnpcc.cpp @@ -0,0 +1,15 @@ +#include "convertnpcc.hpp" + +#include "convertinventory.hpp" + +namespace ESSImport +{ + + void convertNPCC(const NPCC &npcc, ESM::NpcState &npcState) + { + npcState.mNpcStats.mDisposition = npcc.mNPDT.mDisposition; + npcState.mNpcStats.mReputation = npcc.mNPDT.mReputation; + + convertInventory(npcc.mInventory, npcState.mInventory); + } +} diff --git a/apps/essimporter/convertnpcc.hpp b/apps/essimporter/convertnpcc.hpp new file mode 100644 index 000000000..eb12d8f3b --- /dev/null +++ b/apps/essimporter/convertnpcc.hpp @@ -0,0 +1,15 @@ +#ifndef OPENMW_ESSIMPORT_CONVERTNPCC_H +#define OPENMW_ESSIMPORT_CONVERTNPCC_H + +#include "importnpcc.hpp" + +#include + +namespace ESSImport +{ + + void convertNPCC (const NPCC& npcc, ESM::NpcState& npcState); + +} + +#endif diff --git a/apps/essimporter/convertplayer.cpp b/apps/essimporter/convertplayer.cpp new file mode 100644 index 000000000..532efa7f5 --- /dev/null +++ b/apps/essimporter/convertplayer.cpp @@ -0,0 +1,38 @@ +#include "convertplayer.hpp" + +namespace ESSImport +{ + + void convertPCDT(const PCDT& pcdt, ESM::Player& out, std::vector& outDialogueTopics, bool& firstPersonCam) + { + out.mBirthsign = pcdt.mBirthsign; + out.mObject.mNpcStats.mBounty = pcdt.mBounty; + for (std::vector::const_iterator it = pcdt.mFactions.begin(); it != pcdt.mFactions.end(); ++it) + { + ESM::NpcStats::Faction faction; + faction.mExpelled = (it->mFlags & 0x2) != 0; + faction.mRank = it->mRank; + faction.mReputation = it->mReputation; + out.mObject.mNpcStats.mFactions[Misc::StringUtils::lowerCase(it->mFactionName.toString())] = faction; + } + for (int i=0; i<8; ++i) + out.mObject.mNpcStats.mSkillIncrease[i] = pcdt.mPNAM.mSkillIncreases[i]; + for (int i=0; i<27; ++i) + out.mObject.mNpcStats.mSkills[i].mRegular.mProgress = pcdt.mPNAM.mSkillProgress[i]; + out.mObject.mNpcStats.mLevelProgress = pcdt.mPNAM.mLevelProgress; + + if (pcdt.mPNAM.mDrawState & PCDT::DrawState_Weapon) + out.mObject.mCreatureStats.mDrawState = 1; + if (pcdt.mPNAM.mDrawState & PCDT::DrawState_Spell) + out.mObject.mCreatureStats.mDrawState = 2; + + firstPersonCam = (pcdt.mPNAM.mCameraState == PCDT::CameraState_FirstPerson); + + for (std::vector::const_iterator it = pcdt.mKnownDialogueTopics.begin(); + it != pcdt.mKnownDialogueTopics.end(); ++it) + { + outDialogueTopics.push_back(Misc::StringUtils::lowerCase(*it)); + } + } + +} diff --git a/apps/essimporter/convertplayer.hpp b/apps/essimporter/convertplayer.hpp new file mode 100644 index 000000000..f6731eed7 --- /dev/null +++ b/apps/essimporter/convertplayer.hpp @@ -0,0 +1,15 @@ +#ifndef OPENMW_ESSIMPORT_CONVERTPLAYER_H +#define OPENMW_ESSIMPORT_CONVERTPLAYER_H + +#include "importplayer.hpp" + +#include + +namespace ESSImport +{ + + void convertPCDT(const PCDT& pcdt, ESM::Player& out, std::vector& outDialogueTopics, bool& firstPersonCam); + +} + +#endif diff --git a/apps/essimporter/convertscpt.cpp b/apps/essimporter/convertscpt.cpp new file mode 100644 index 000000000..ca81ebbbf --- /dev/null +++ b/apps/essimporter/convertscpt.cpp @@ -0,0 +1,17 @@ +#include "convertscpt.hpp" + +#include + +#include "convertscri.hpp" + +namespace ESSImport +{ + + void convertSCPT(const SCPT &scpt, ESM::GlobalScript &out) + { + out.mId = Misc::StringUtils::lowerCase(scpt.mSCHD.mName.toString()); + out.mRunning = scpt.mRunning; + convertSCRI(scpt.mSCRI, out.mLocals); + } + +} diff --git a/apps/essimporter/convertscpt.hpp b/apps/essimporter/convertscpt.hpp new file mode 100644 index 000000000..3390bd607 --- /dev/null +++ b/apps/essimporter/convertscpt.hpp @@ -0,0 +1,15 @@ +#ifndef OPENMW_ESSIMPORT_CONVERTSCPT_H +#define OPENMW_ESSIMPORT_CONVERTSCPT_H + +#include + +#include "importscpt.hpp" + +namespace ESSImport +{ + +void convertSCPT(const SCPT& scpt, ESM::GlobalScript& out); + +} + +#endif diff --git a/apps/essimporter/convertscri.cpp b/apps/essimporter/convertscri.cpp new file mode 100644 index 000000000..dbe35a010 --- /dev/null +++ b/apps/essimporter/convertscri.cpp @@ -0,0 +1,32 @@ +#include "convertscri.hpp" + +#include + +namespace +{ + + template + void storeVariables(const std::vector& variables, ESM::Locals& locals, const std::string& scriptname) + { + for (typename std::vector::const_iterator it = variables.begin(); it != variables.end(); ++it) + { + ESM::Variant val(*it); + val.setType(VariantType); + locals.mVariables.push_back(std::make_pair(std::string(), val)); + } + } + +} + +namespace ESSImport +{ + + void convertSCRI(const SCRI &scri, ESM::Locals &locals) + { + // order *is* important, as we do not have variable names available in this format + storeVariables (scri.mShorts, locals, scri.mScript); + storeVariables (scri.mLongs, locals, scri.mScript); + storeVariables (scri.mFloats, locals, scri.mScript); + } + +} diff --git a/apps/essimporter/convertscri.hpp b/apps/essimporter/convertscri.hpp new file mode 100644 index 000000000..2d8945666 --- /dev/null +++ b/apps/essimporter/convertscri.hpp @@ -0,0 +1,16 @@ +#ifndef OPENMW_ESSIMPORT_CONVERTSCRI_H +#define OPENMW_ESSIMPORT_CONVERTSCRI_H + +#include "importscri.hpp" + +#include + +namespace ESSImport +{ + + /// Convert script variable assignments + void convertSCRI (const SCRI& scri, ESM::Locals& locals); + +} + +#endif diff --git a/apps/essimporter/importacdt.cpp b/apps/essimporter/importacdt.cpp new file mode 100644 index 000000000..9d881515d --- /dev/null +++ b/apps/essimporter/importacdt.cpp @@ -0,0 +1,115 @@ +#include "importacdt.hpp" + +#include + +#include + +namespace ESSImport +{ + + void ActorData::load(ESM::ESMReader &esm) + { + if (esm.isNextSub("ACTN")) + esm.skipHSub(); + + if (esm.isNextSub("STPR")) + esm.skipHSub(); + + if (esm.isNextSub("MNAM")) + esm.skipHSub(); + + ESM::CellRef::loadData(esm); + + mHasACDT = false; + if (esm.isNextSub("ACDT")) + { + mHasACDT = true; + esm.getHT(mACDT); + } + + mHasACSC = false; + if (esm.isNextSub("ACSC")) + { + mHasACSC = true; + esm.getHT(mACSC); + } + + if (esm.isNextSub("ACSL")) + esm.skipHSubSize(112); + + if (esm.isNextSub("CSTN")) + esm.skipHSub(); // "PlayerSaveGame", link to some object? + + if (esm.isNextSub("LSTN")) + esm.skipHSub(); // "PlayerSaveGame", link to some object? + + // unsure at which point between LSTN and TGTN + if (esm.isNextSub("CSHN")) + esm.skipHSub(); // "PlayerSaveGame", link to some object? + + // unsure if before or after CSTN/LSTN + if (esm.isNextSub("LSHN")) + esm.skipHSub(); // "PlayerSaveGame", link to some object? + + while (esm.isNextSub("TGTN")) + esm.skipHSub(); // "PlayerSaveGame", link to some object? + + while (esm.isNextSub("FGTN")) + esm.getHString(); // fight target? + + // unsure at which point between TGTN and CRED + if (esm.isNextSub("AADT")) + { + // occured when a creature was in the middle of its attack, 44 bytes + esm.skipHSub(); + } + + // unsure at which point between FGTN and CHRD + if (esm.isNextSub("PWPC")) + esm.skipHSub(); + if (esm.isNextSub("PWPS")) + esm.skipHSub(); + + if (esm.isNextSub("WNAM")) + { + std::string id = esm.getHString(); + + if (esm.isNextSub("XNAM")) + mSelectedEnchantItem = esm.getHString(); + else + mSelectedSpell = id; + + if (esm.isNextSub("YNAM")) + esm.skipHSub(); // 4 byte, 0 + } + + while (esm.isNextSub("APUD")) + { + // used power + esm.getSubHeader(); + std::string id = esm.getString(32); + (void)id; + // timestamp can't be used: this is the total hours passed, calculated by + // timestamp = 24 * (365 * year + cumulativeDays[month] + day) + // unfortunately cumulativeDays[month] is not clearly defined, + // in the (non-MCP) vanilla version the first month was missing, but MCP added it. + double timestamp; + esm.getT(timestamp); + } + + // FIXME: not all actors have this, add flag + if (esm.isNextSub("CHRD")) // npc only + esm.getHExact(mSkills, 27*2*sizeof(int)); + + if (esm.isNextSub("CRED")) // creature only + esm.getHExact(mCombatStats, 3*2*sizeof(int)); + + mSCRI.load(esm); + + if (esm.isNextSub("ND3D")) + esm.skipHSub(); + if (esm.isNextSub("ANIS")) + esm.skipHSub(); + } + +} diff --git a/apps/essimporter/importacdt.hpp b/apps/essimporter/importacdt.hpp new file mode 100644 index 000000000..eacb2edf1 --- /dev/null +++ b/apps/essimporter/importacdt.hpp @@ -0,0 +1,85 @@ +#ifndef OPENMW_ESSIMPORT_ACDT_H +#define OPENMW_ESSIMPORT_ACDT_H + +#include + +#include + +#include "importscri.hpp" + +namespace ESM +{ + class ESMReader; +} + +namespace ESSImport +{ + + enum ACDTFlags + { + TalkedToPlayer = 0x4, + Attacked = 0x100, + Unknown = 0x200 + }; + enum ACSCFlags + { + Dead = 0x2 + }; + + /// Actor data, shared by (at least) REFR and CellRef +#pragma pack(push) +#pragma pack(1) + struct ACDT + { + // Note, not stored at *all*: + // - Level changes are lost on reload, except for the player (there it's in the NPC record). + unsigned char mUnknown[12]; + unsigned int mFlags; + float mBreathMeter; // Seconds left before drowning + unsigned char mUnknown2[20]; + float mDynamic[3][2]; + unsigned char mUnknown3[16]; + float mAttributes[8][2]; + float mMagicEffects[27]; // Effect attributes: https://wiki.openmw.org/index.php?title=Research:Magic#Effect_attributes + unsigned char mUnknown4[4]; + unsigned int mGoldPool; + unsigned char mCountDown; // seen the same value as in ACSC.mCorpseClearCountdown, maybe + // this one is for respawning? + unsigned char mUnknown5[3]; + }; + struct ACSC + { + unsigned char mUnknown1[17]; + unsigned char mFlags; // ACSCFlags + unsigned char mUnknown2[22]; + unsigned char mCorpseClearCountdown; // hours? + unsigned char mUnknown3[71]; + }; +#pragma pack(pop) + + struct ActorData : public ESM::CellRef + { + bool mHasACDT; + ACDT mACDT; + + bool mHasACSC; + ACSC mACSC; + + int mSkills[27][2]; // skills, base and modified + + // creature combat stats, base and modified + // I think these can be ignored in the conversion, because it is not possible + // to change them ingame + int mCombatStats[3][2]; + + std::string mSelectedSpell; + std::string mSelectedEnchantItem; + + SCRI mSCRI; + + void load(ESM::ESMReader& esm); + }; + +} + +#endif diff --git a/apps/essimporter/importcellref.cpp b/apps/essimporter/importcellref.cpp new file mode 100644 index 000000000..cca356b2a --- /dev/null +++ b/apps/essimporter/importcellref.cpp @@ -0,0 +1,57 @@ +#include "importcellref.hpp" + +#include + +namespace ESSImport +{ + + void CellRef::load(ESM::ESMReader &esm) + { + blank(); + + // (FRMR subrecord name is already read by the loop in ConvertCell) + esm.getHT(mRefNum.mIndex); // FRMR + + // this is required since openmw supports more than 255 content files + int pluginIndex = (mRefNum.mIndex & 0xff000000) >> 24; + mRefNum.mContentFile = pluginIndex-1; + mRefNum.mIndex &= 0x00ffffff; + + mIndexedRefId = esm.getHNString("NAME"); + + ActorData::load(esm); + if (esm.isNextSub("LVCR")) + { + // occurs on levelled creature spawner references + // probably some identifier for the creature that has been spawned? + unsigned char lvcr; + esm.getHT(lvcr); + //std::cout << "LVCR: " << (int)lvcr << std::endl; + } + + mEnabled = true; + esm.getHNOT(mEnabled, "ZNAM"); + + // DATA should occur for all references, except levelled creature spawners + // I've seen DATA *twice* on a creature record, and with the exact same content too! weird + // alarmvoi0000.ess + esm.getHNOT(mPos, "DATA", 24); + esm.getHNOT(mPos, "DATA", 24); + + mDeleted = 0; + if (esm.isNextSub("DELE")) + { + unsigned int deleted; + esm.getHT(deleted); + mDeleted = (deleted >> 24) & 0x2; // the other 3 bytes seem to be uninitialized garbage + } + + if (esm.isNextSub("MVRF")) + { + esm.skipHSub(); + esm.getSubName(); + esm.skipHSub(); + } + } + +} diff --git a/apps/essimporter/importcellref.hpp b/apps/essimporter/importcellref.hpp new file mode 100644 index 000000000..556ed19bf --- /dev/null +++ b/apps/essimporter/importcellref.hpp @@ -0,0 +1,33 @@ +#ifndef OPENMW_ESSIMPORT_CELLREF_H +#define OPENMW_ESSIMPORT_CELLREF_H + +#include + +#include + +#include "importacdt.hpp" + +namespace ESM +{ + class ESMReader; +} + +namespace ESSImport +{ + + struct CellRef : public ActorData + { + std::string mIndexedRefId; + + std::string mScript; + + bool mEnabled; + + bool mDeleted; + + void load(ESM::ESMReader& esm); + }; + +} + +#endif diff --git a/apps/essimporter/importcntc.cpp b/apps/essimporter/importcntc.cpp new file mode 100644 index 000000000..a492aef5a --- /dev/null +++ b/apps/essimporter/importcntc.cpp @@ -0,0 +1,16 @@ +#include "importcntc.hpp" + +#include + +namespace ESSImport +{ + + void CNTC::load(ESM::ESMReader &esm) + { + mIndex = 0; + esm.getHNT(mIndex, "INDX"); + + mInventory.load(esm); + } + +} diff --git a/apps/essimporter/importcntc.hpp b/apps/essimporter/importcntc.hpp new file mode 100644 index 000000000..1bc7d94bd --- /dev/null +++ b/apps/essimporter/importcntc.hpp @@ -0,0 +1,25 @@ +#ifndef OPENMW_ESSIMPORT_IMPORTCNTC_H +#define OPENMW_ESSIMPORT_IMPORTCNTC_H + +#include "importinventory.hpp" + +namespace ESM +{ + class ESMReader; +} + +namespace ESSImport +{ + + /// Changed container contents + struct CNTC + { + int mIndex; + + Inventory mInventory; + + void load(ESM::ESMReader& esm); + }; + +} +#endif diff --git a/apps/essimporter/importcrec.cpp b/apps/essimporter/importcrec.cpp new file mode 100644 index 000000000..64879f2af --- /dev/null +++ b/apps/essimporter/importcrec.cpp @@ -0,0 +1,25 @@ +#include "importcrec.hpp" + +#include + +namespace ESSImport +{ + + void CREC::load(ESM::ESMReader &esm) + { + esm.getHNT(mIndex, "INDX"); + + // equivalent of ESM::Creature XSCL? probably don't have to convert this, + // since the value can't be changed + float scale; + esm.getHNOT(scale, "XSCL"); + + + while (esm.isNextSub("AI_W") || esm.isNextSub("AI_E") || esm.isNextSub("AI_T") || esm.isNextSub("AI_F") + || esm.isNextSub("AI_A")) + mAiPackages.add(esm); + + mInventory.load(esm); + } + +} diff --git a/apps/essimporter/importcrec.hpp b/apps/essimporter/importcrec.hpp new file mode 100644 index 000000000..5110fbc68 --- /dev/null +++ b/apps/essimporter/importcrec.hpp @@ -0,0 +1,28 @@ +#ifndef OPENMW_ESSIMPORT_CREC_H +#define OPENMW_ESSIMPORT_CREC_H + +#include "importinventory.hpp" +#include + +namespace ESM +{ + class ESMReader; +} + +namespace ESSImport +{ + + /// Creature changes + struct CREC + { + int mIndex; + + Inventory mInventory; + ESM::AIPackageList mAiPackages; + + void load(ESM::ESMReader& esm); + }; + +} + +#endif diff --git a/apps/essimporter/importdial.cpp b/apps/essimporter/importdial.cpp new file mode 100644 index 000000000..5797a708a --- /dev/null +++ b/apps/essimporter/importdial.cpp @@ -0,0 +1,23 @@ +#include "importdial.hpp" + +#include + +namespace ESSImport +{ + + void DIAL::load(ESM::ESMReader &esm) + { + // See ESM::Dialogue::Type enum, not sure why we would need this here though + int type = 0; + esm.getHNOT(type, "DATA"); + + // Deleted dialogue in a savefile. No clue what this means... + int deleted = 0; + esm.getHNOT(deleted, "DELE"); + + mIndex = 0; + // *should* always occur except when the dialogue is deleted, but leaving it optional just in case... + esm.getHNOT(mIndex, "XIDX"); + } + +} diff --git a/apps/essimporter/importdial.hpp b/apps/essimporter/importdial.hpp new file mode 100644 index 000000000..9a1e88233 --- /dev/null +++ b/apps/essimporter/importdial.hpp @@ -0,0 +1,20 @@ +#ifndef OPENMW_ESSIMPORT_IMPORTDIAL_H +#define OPENMW_ESSIMPORT_IMPORTDIAL_H +namespace ESM +{ + class ESMReader; +} + +namespace ESSImport +{ + + struct DIAL + { + int mIndex; // Journal index + + void load(ESM::ESMReader& esm); + }; + +} + +#endif diff --git a/apps/essimporter/importer.cpp b/apps/essimporter/importer.cpp new file mode 100644 index 000000000..d5ed43b8a --- /dev/null +++ b/apps/essimporter/importer.cpp @@ -0,0 +1,378 @@ +#include "importer.hpp" +#include + +#include + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "importercontext.hpp" + +#include "converter.hpp" + +namespace +{ + + void writeScreenshot(const ESM::Header& fileHeader, ESM::SavedGame& out) + { + Ogre::Image screenshot; + std::vector screenshotData = fileHeader.mSCRS; // MemoryDataStream doesn't work with const data :( + Ogre::DataStreamPtr screenshotStream (new Ogre::MemoryDataStream(&screenshotData[0], screenshotData.size())); + screenshot.loadRawData(screenshotStream, 128, 128, 1, Ogre::PF_BYTE_BGRA); + Ogre::DataStreamPtr encoded = screenshot.encode("jpg"); + out.mScreenshot.resize(encoded->size()); + encoded->read(&out.mScreenshot[0], encoded->size()); + } + +} + +namespace ESSImport +{ + + Importer::Importer(const std::string &essfile, const std::string &outfile, const std::string &encoding) + : mEssFile(essfile) + , mOutFile(outfile) + , mEncoding(encoding) + { + + } + + struct File + { + struct Subrecord + { + std::string mName; + size_t mFileOffset; + std::vector mData; + }; + + struct Record + { + std::string mName; + size_t mFileOffset; + std::vector mSubrecords; + }; + + std::vector mRecords; + }; + + void read(const std::string& filename, File& file) + { + ESM::ESMReader esm; + esm.open(filename); + + while (esm.hasMoreRecs()) + { + ESM::NAME n = esm.getRecName(); + esm.getRecHeader(); + + File::Record rec; + rec.mName = n.toString(); + rec.mFileOffset = esm.getFileOffset(); + while (esm.hasMoreSubs()) + { + File::Subrecord sub; + esm.getSubName(); + esm.getSubHeader(); + sub.mFileOffset = esm.getFileOffset(); + sub.mName = esm.retSubName().toString(); + sub.mData.resize(esm.getSubSize()); + esm.getExact(&sub.mData[0], sub.mData.size()); + rec.mSubrecords.push_back(sub); + } + file.mRecords.push_back(rec); + } + } + + void Importer::compare() + { + // data that always changes (and/or is already fully decoded) should be blacklisted + std::set > blacklist; + blacklist.insert(std::make_pair("GLOB", "FLTV")); // gamehour + blacklist.insert(std::make_pair("REFR", "DATA")); // player position + blacklist.insert(std::make_pair("CELL", "NAM8")); // fog of war + blacklist.insert(std::make_pair("GAME", "GMDT")); // weather data, current time always changes + blacklist.insert(std::make_pair("CELL", "DELE")); // first 3 bytes are uninitialized + + // this changes way too often, name suggests some renderer internal data? + blacklist.insert(std::make_pair("CELL", "ND3D")); + blacklist.insert(std::make_pair("REFR", "ND3D")); + + File file1; + read(mEssFile, file1); + File file2; + read(mOutFile, file2); // todo rename variable + + // FIXME: use max(size1, size2) + for (unsigned int i=0; i= file2.mRecords.size()) + { + std::cout << "Record in file1 not present in file2: (1) 0x" << std::hex << rec.mFileOffset << std::endl; + return; + } + + File::Record rec2 = file2.mRecords[i]; + + if (rec.mName != rec2.mName) + { + std::cout << "Different record name at (2) 0x" << std::hex << rec2.mFileOffset << std::endl; + return; // TODO: try to recover + } + + // FIXME: use max(size1, size2) + for (unsigned int j=0; j= rec2.mSubrecords.size()) + { + std::cout << "Subrecord in file1 not present in file2: (1) 0x" << std::hex << sub.mFileOffset << std::endl; + return; + } + + File::Subrecord sub2 = rec2.mSubrecords[j]; + + if (sub.mName != sub2.mName) + { + std::cout << "Different subrecord name (" << rec.mName << "." << sub.mName << " vs. " << sub2.mName << ") at (1) 0x" << std::hex << sub.mFileOffset + << " (2) 0x" << sub2.mFileOffset << std::endl; + break; // TODO: try to recover + } + + if (sub.mData != sub2.mData) + { + if (blacklist.find(std::make_pair(rec.mName, sub.mName)) != blacklist.end()) + continue; + + std::cout << "Different subrecord data for " << rec.mName << "." << sub.mName << " at (1) 0x" << std::hex << sub.mFileOffset + << " (2) 0x" << sub2.mFileOffset << std::endl; + + std::cout << "Data 1:" << std::endl; + for (unsigned int k=0; k= sub2.mData.size() || sub2.mData[k] != sub.mData[k]) + different = true; + + if (different) + std::cout << "\033[033m"; + std::cout << std::hex << std::setw(2) << std::setfill('0') << (int)sub.mData[k] << " "; + if (different) + std::cout << "\033[0m"; + } + std::cout << std::endl; + + std::cout << "Data 2:" << std::endl; + for (unsigned int k=0; k= sub.mData.size() || sub.mData[k] != sub2.mData[k]) + different = true; + + if (different) + std::cout << "\033[033m"; + std::cout << std::hex << std::setw(2) << std::setfill('0') << (int)sub2.mData[k] << " "; + if (different) + std::cout << "\033[0m"; + } + std::cout << std::endl; + } + } + } + } + + void Importer::run() + { + // construct Ogre::Root to gain access to image codecs + Ogre::LogManager logman; + Ogre::Root root; + + ToUTF8::Utf8Encoder encoder(ToUTF8::calculateEncoding(mEncoding)); + ESM::ESMReader esm; + esm.open(mEssFile); + esm.setEncoder(&encoder); + + Context context; + + const ESM::Header& header = esm.getHeader(); + context.mPlayerCellName = header.mGameData.mCurrentCell.toString(); + + const unsigned int recREFR = ESM::FourCC<'R','E','F','R'>::value; + const unsigned int recPCDT = ESM::FourCC<'P','C','D','T'>::value; + const unsigned int recFMAP = ESM::FourCC<'F','M','A','P'>::value; + const unsigned int recKLST = ESM::FourCC<'K','L','S','T'>::value; + const unsigned int recSTLN = ESM::FourCC<'S','T','L','N'>::value; + const unsigned int recGAME = ESM::FourCC<'G','A','M','E'>::value; + const unsigned int recJOUR = ESM::FourCC<'J','O','U','R'>::value; + + std::map > converters; + converters[ESM::REC_GLOB] = boost::shared_ptr(new ConvertGlobal()); + converters[ESM::REC_BOOK] = boost::shared_ptr(new ConvertBook()); + converters[ESM::REC_NPC_] = boost::shared_ptr(new ConvertNPC()); + converters[ESM::REC_CREA] = boost::shared_ptr(new ConvertCREA()); + converters[ESM::REC_NPCC] = boost::shared_ptr(new ConvertNPCC()); + converters[ESM::REC_CREC] = boost::shared_ptr(new ConvertCREC()); + converters[recREFR ] = boost::shared_ptr(new ConvertREFR()); + converters[recPCDT ] = boost::shared_ptr(new ConvertPCDT()); + converters[recFMAP ] = boost::shared_ptr(new ConvertFMAP()); + converters[recKLST ] = boost::shared_ptr(new ConvertKLST()); + converters[recSTLN ] = boost::shared_ptr(new ConvertSTLN()); + converters[recGAME ] = boost::shared_ptr(new ConvertGAME()); + converters[ESM::REC_CELL] = boost::shared_ptr(new ConvertCell()); + converters[ESM::REC_ALCH] = boost::shared_ptr(new DefaultConverter()); + converters[ESM::REC_CLAS] = boost::shared_ptr(new ConvertClass()); + converters[ESM::REC_SPEL] = boost::shared_ptr(new DefaultConverter()); + converters[ESM::REC_ARMO] = boost::shared_ptr(new DefaultConverter()); + converters[ESM::REC_WEAP] = boost::shared_ptr(new DefaultConverter()); + converters[ESM::REC_CLOT] = boost::shared_ptr(new DefaultConverter()); + converters[ESM::REC_ENCH] = boost::shared_ptr(new DefaultConverter()); + converters[ESM::REC_WEAP] = boost::shared_ptr(new DefaultConverter()); + converters[ESM::REC_LEVC] = boost::shared_ptr(new DefaultConverter()); + converters[ESM::REC_LEVI] = boost::shared_ptr(new DefaultConverter()); + converters[ESM::REC_CNTC] = boost::shared_ptr(new ConvertCNTC()); + converters[ESM::REC_FACT] = boost::shared_ptr(new ConvertFACT()); + converters[ESM::REC_INFO] = boost::shared_ptr(new ConvertINFO()); + converters[ESM::REC_DIAL] = boost::shared_ptr(new ConvertDIAL()); + converters[ESM::REC_QUES] = boost::shared_ptr(new ConvertQUES()); + converters[recJOUR ] = boost::shared_ptr(new ConvertJOUR()); + converters[ESM::REC_SCPT] = boost::shared_ptr(new ConvertSCPT()); + + // TODO: + // - REGN (weather in certain regions?) + // - VFXM + // - SPLM (active spell effects) + // - PROJ (magic projectiles in air) + + std::set unknownRecords; + + for (std::map >::const_iterator it = converters.begin(); + it != converters.end(); ++it) + { + it->second->setContext(context); + } + + while (esm.hasMoreRecs()) + { + ESM::NAME n = esm.getRecName(); + esm.getRecHeader(); + + std::map >::iterator it = converters.find(n.val); + if (it != converters.end()) + { + it->second->read(esm); + } + else + { + if (unknownRecords.insert(n.val).second) + std::cerr << "unknown record " << n.toString() << " (0x" << std::hex << esm.getFileOffset() << ")" << std::endl; + + esm.skipRecord(); + } + } + + ESM::ESMWriter writer; + + writer.setFormat (ESM::Header::CurrentFormat); + + std::ofstream stream(mOutFile.c_str(), std::ios::binary); + // all unused + writer.setVersion(0); + writer.setType(0); + writer.setAuthor(""); + writer.setDescription(""); + writer.setRecordCount (0); + + for (std::vector::const_iterator it = header.mMaster.begin(); + it != header.mMaster.end(); ++it) + writer.addMaster (it->name, 0); // not using the size information anyway -> use value of 0 + + writer.save (stream); + + ESM::SavedGame profile; + for (std::vector::const_iterator it = header.mMaster.begin(); + it != header.mMaster.end(); ++it) + { + profile.mContentFiles.push_back(it->name); + } + profile.mDescription = esm.getDesc(); + profile.mInGameTime.mDay = context.mDay; + profile.mInGameTime.mGameHour = context.mHour; + profile.mInGameTime.mMonth = context.mMonth; + profile.mInGameTime.mYear = context.mYear; + profile.mPlayerCell = header.mGameData.mCurrentCell.toString(); + if (context.mPlayerBase.mClass == "NEWCLASSID_CHARGEN") + profile.mPlayerClassName = context.mCustomPlayerClassName; + else + profile.mPlayerClassId = context.mPlayerBase.mClass; + profile.mPlayerLevel = context.mPlayerBase.mNpdt52.mLevel; + profile.mPlayerName = header.mGameData.mPlayerName.toString(); + + writeScreenshot(header, profile); + + writer.startRecord (ESM::REC_SAVE); + profile.save (writer); + writer.endRecord (ESM::REC_SAVE); + + // Writing order should be Dynamic Store -> Cells -> Player, + // so that references to dynamic records can be recognized when loading + for (std::map >::const_iterator it = converters.begin(); + it != converters.end(); ++it) + { + if (it->second->getStage() != 0) + continue; + it->second->write(writer); + } + + writer.startRecord(ESM::REC_NPC_); + writer.writeHNString("NAME", "player"); + context.mPlayerBase.save(writer); + writer.endRecord(ESM::REC_NPC_); + + for (std::map >::const_iterator it = converters.begin(); + it != converters.end(); ++it) + { + if (it->second->getStage() != 1) + continue; + it->second->write(writer); + } + + writer.startRecord(ESM::REC_PLAY); + if (context.mPlayer.mCellId.mPaged) + { + // exterior cell -> determine cell coordinates based on position + const int cellSize = 8192; + int cellX = static_cast(std::floor(context.mPlayer.mObject.mPosition.pos[0]/cellSize)); + int cellY = static_cast(std::floor(context.mPlayer.mObject.mPosition.pos[1] / cellSize)); + context.mPlayer.mCellId.mIndex.mX = cellX; + context.mPlayer.mCellId.mIndex.mY = cellY; + } + context.mPlayer.save(writer); + writer.endRecord(ESM::REC_PLAY); + + writer.startRecord (ESM::REC_DIAS); + context.mDialogueState.save(writer); + writer.endRecord(ESM::REC_DIAS); + } + + +} diff --git a/apps/essimporter/importer.hpp b/apps/essimporter/importer.hpp new file mode 100644 index 000000000..ccacd7972 --- /dev/null +++ b/apps/essimporter/importer.hpp @@ -0,0 +1,26 @@ +#ifndef OPENMW_ESSIMPORTER_IMPORTER_H +#define OPENMW_ESSIMPORTER_IMPORTER_H + +#include + +namespace ESSImport +{ + + class Importer + { + public: + Importer(const std::string& essfile, const std::string& outfile, const std::string& encoding); + + void run(); + + void compare(); + + private: + std::string mEssFile; + std::string mOutFile; + std::string mEncoding; + }; + +} + +#endif diff --git a/apps/openmw/mwgui/keywordsearch.cpp b/apps/essimporter/importercontext.cpp similarity index 100% rename from apps/openmw/mwgui/keywordsearch.cpp rename to apps/essimporter/importercontext.cpp diff --git a/apps/essimporter/importercontext.hpp b/apps/essimporter/importercontext.hpp new file mode 100644 index 000000000..3b010cb8f --- /dev/null +++ b/apps/essimporter/importercontext.hpp @@ -0,0 +1,73 @@ +#ifndef OPENMW_ESSIMPORT_CONTEXT_H +#define OPENMW_ESSIMPORT_CONTEXT_H + +#include + +#include +#include +#include +#include +#include +#include + +#include "importnpcc.hpp" +#include "importcrec.hpp" +#include "importcntc.hpp" +#include "importplayer.hpp" + + + + +namespace ESSImport +{ + + struct Context + { + // set from the TES3 header + std::string mPlayerCellName; + + ESM::Player mPlayer; + ESM::NPC mPlayerBase; + std::string mCustomPlayerClassName; + + ESM::DialogueState mDialogueState; + + // cells which should show an explored overlay on the global map + std::set > mExploredCells; + + ESM::GlobalMap mGlobalMapState; + + int mDay, mMonth, mYear; + float mHour; + + // key + std::map, CREC> mCreatureChanges; + std::map, NPCC> mNpcChanges; + std::map, CNTC> mContainerChanges; + + std::map mCreatures; + std::map mNpcs; + + Context() + { + mPlayer.mAutoMove = 0; + ESM::CellId playerCellId; + playerCellId.mPaged = true; + playerCellId.mIndex.mX = playerCellId.mIndex.mY = 0; + mPlayer.mCellId = playerCellId; + //mPlayer.mLastKnownExteriorPosition + mPlayer.mHasMark = 0; // TODO + mPlayer.mCurrentCrimeId = 0; // TODO + mPlayer.mObject.blank(); + mPlayer.mObject.mRef.mRefID = "player"; // REFR.mRefID would be PlayerSaveGame + + mGlobalMapState.mBounds.mMinX = 0; + mGlobalMapState.mBounds.mMaxX = 0; + mGlobalMapState.mBounds.mMinY = 0; + mGlobalMapState.mBounds.mMaxY = 0; + } + }; + +} + +#endif diff --git a/apps/essimporter/importgame.cpp b/apps/essimporter/importgame.cpp new file mode 100644 index 000000000..1012541b4 --- /dev/null +++ b/apps/essimporter/importgame.cpp @@ -0,0 +1,29 @@ +#include "importgame.hpp" + +#include + +namespace ESSImport +{ + +void GAME::load(ESM::ESMReader &esm) +{ + esm.getSubNameIs("GMDT"); + esm.getSubHeader(); + if (esm.getSubSize() == 92) + { + esm.getExact(&mGMDT, 92); + mGMDT.mSecundaPhase = 0; + } + else if (esm.getSubSize() == 96) + { + esm.getT(mGMDT); + } + else + esm.fail("unexpected subrecord size for GAME.GMDT"); + + mGMDT.mWeatherTransition &= (0x000000ff); + mGMDT.mSecundaPhase &= (0x000000ff); + mGMDT.mMasserPhase &= (0x000000ff); +} + +} diff --git a/apps/essimporter/importgame.hpp b/apps/essimporter/importgame.hpp new file mode 100644 index 000000000..fca7d72a0 --- /dev/null +++ b/apps/essimporter/importgame.hpp @@ -0,0 +1,33 @@ +#ifndef OPENMW_ESSIMPORT_GAME_H +#define OPENMW_ESSIMPORT_GAME_H + +namespace ESM +{ + class ESMReader; +} + +namespace ESSImport +{ + + /// Weather data + struct GAME + { + struct GMDT + { + char mCellName[64]; + int mFogColour; + float mFogDensity; + int mCurrentWeather, mNextWeather; + int mWeatherTransition; // 0-100 transition between weathers, top 3 bytes may be garbage + float mTimeOfNextTransition; // weather changes when gamehour == timeOfNextTransition + int mMasserPhase, mSecundaPhase; // top 3 bytes may be garbage + }; + + GMDT mGMDT; + + void load(ESM::ESMReader& esm); + }; + +} + +#endif diff --git a/apps/essimporter/importinfo.cpp b/apps/essimporter/importinfo.cpp new file mode 100644 index 000000000..113155370 --- /dev/null +++ b/apps/essimporter/importinfo.cpp @@ -0,0 +1,14 @@ +#include "importinfo.hpp" + +#include + +namespace ESSImport +{ + + void INFO::load(ESM::ESMReader &esm) + { + mInfo = esm.getHNString("INAM"); + mActorRefId = esm.getHNString("ACDT"); + } + +} diff --git a/apps/essimporter/importinfo.hpp b/apps/essimporter/importinfo.hpp new file mode 100644 index 000000000..f4d616786 --- /dev/null +++ b/apps/essimporter/importinfo.hpp @@ -0,0 +1,24 @@ +#ifndef OPENMW_ESSIMPORT_IMPORTINFO_H +#define OPENMW_ESSIMPORT_IMPORTINFO_H + +#include + +namespace ESM +{ + class ESMReader; +} + +namespace ESSImport +{ + + struct INFO + { + std::string mInfo; + std::string mActorRefId; + + void load(ESM::ESMReader& esm); + }; + +} + +#endif diff --git a/apps/essimporter/importinventory.cpp b/apps/essimporter/importinventory.cpp new file mode 100644 index 000000000..d27cd5c8c --- /dev/null +++ b/apps/essimporter/importinventory.cpp @@ -0,0 +1,68 @@ +#include "importinventory.hpp" + +#include + +#include + +#include + +namespace ESSImport +{ + + void Inventory::load(ESM::ESMReader &esm) + { + while (esm.isNextSub("NPCO")) + { + ESM::ContItem contItem; + esm.getHT(contItem); + + InventoryItem item; + item.mId = contItem.mItem.toString(); + item.mCount = contItem.mCount; + item.mRelativeEquipmentSlot = -1; + + // seems that a stack of items can have a set of subrecords for each item? rings0000.ess + // doesn't make any sense to me, if the values were different then the items shouldn't stack in the first place? + // I guess we should double check the stacking logic in OpenMW + for (int i=0;i= int(mItems.size())) + esm.fail("equipment item index out of range"); + + // appears to be a relative index for only the *possible* slots this item can be equipped in, + // i.e. 0 most of the time + int slotIndex; + esm.getT(slotIndex); + + mItems[itemIndex].mRelativeEquipmentSlot = slotIndex; + } + } + +} diff --git a/apps/essimporter/importinventory.hpp b/apps/essimporter/importinventory.hpp new file mode 100644 index 000000000..0b5405d96 --- /dev/null +++ b/apps/essimporter/importinventory.hpp @@ -0,0 +1,34 @@ +#ifndef OPENMW_ESSIMPORT_IMPORTINVENTORY_H +#define OPENMW_ESSIMPORT_IMPORTINVENTORY_H + +#include +#include + +#include +#include "importscri.hpp" + +namespace ESM +{ + class ESMReader; +} + +namespace ESSImport +{ + + struct Inventory + { + struct InventoryItem : public ESM::CellRef + { + std::string mId; + int mCount; + int mRelativeEquipmentSlot; + SCRI mSCRI; + }; + std::vector mItems; + + void load(ESM::ESMReader& esm); + }; + +} + +#endif diff --git a/apps/essimporter/importjour.cpp b/apps/essimporter/importjour.cpp new file mode 100644 index 000000000..e5d24e113 --- /dev/null +++ b/apps/essimporter/importjour.cpp @@ -0,0 +1,13 @@ +#include "importjour.hpp" + +#include + +namespace ESSImport +{ + + void JOUR::load(ESM::ESMReader &esm) + { + mText = esm.getHNString("NAME"); + } + +} diff --git a/apps/essimporter/importjour.hpp b/apps/essimporter/importjour.hpp new file mode 100644 index 000000000..1b2d094f6 --- /dev/null +++ b/apps/essimporter/importjour.hpp @@ -0,0 +1,25 @@ +#ifndef OPENMW_ESSIMPORT_IMPORTJOUR_H +#define OPENMW_ESSIMPORT_IMPORTJOUR_H + +#include + +namespace ESM +{ + class ESMReader; +} + +namespace ESSImport +{ + + /// Journal + struct JOUR + { + // The entire journal, in HTML + std::string mText; + + void load(ESM::ESMReader& esm); + }; + +} + +#endif diff --git a/apps/essimporter/importklst.cpp b/apps/essimporter/importklst.cpp new file mode 100644 index 000000000..daa1ab077 --- /dev/null +++ b/apps/essimporter/importklst.cpp @@ -0,0 +1,22 @@ +#include "importklst.hpp" + +#include + +namespace ESSImport +{ + + void KLST::load(ESM::ESMReader &esm) + { + while (esm.isNextSub("KNAM")) + { + std::string refId = esm.getHString(); + int count; + esm.getHNT(count, "CNAM"); + mKillCounter[refId] = count; + } + + mWerewolfKills = 0; + esm.getHNOT(mWerewolfKills, "INTV"); + } + +} diff --git a/apps/essimporter/importklst.hpp b/apps/essimporter/importklst.hpp new file mode 100644 index 000000000..d07332600 --- /dev/null +++ b/apps/essimporter/importklst.hpp @@ -0,0 +1,28 @@ +#ifndef OPENMW_ESSIMPORT_KLST_H +#define OPENMW_ESSIMPORT_KLST_H + +#include +#include + +namespace ESM +{ + class ESMReader; +} + +namespace ESSImport +{ + + /// Kill Stats + struct KLST + { + void load(ESM::ESMReader& esm); + + /// RefId, kill count + std::map mKillCounter; + + int mWerewolfKills; + }; + +} + +#endif diff --git a/apps/essimporter/importnpcc.cpp b/apps/essimporter/importnpcc.cpp new file mode 100644 index 000000000..3cbd749ce --- /dev/null +++ b/apps/essimporter/importnpcc.cpp @@ -0,0 +1,19 @@ +#include "importnpcc.hpp" + +#include + +namespace ESSImport +{ + + void NPCC::load(ESM::ESMReader &esm) + { + esm.getHNT(mNPDT, "NPDT"); + + while (esm.isNextSub("AI_W") || esm.isNextSub("AI_E") || esm.isNextSub("AI_T") || esm.isNextSub("AI_F") + || esm.isNextSub("AI_A")) + mAiPackages.add(esm); + + mInventory.load(esm); + } + +} diff --git a/apps/essimporter/importnpcc.hpp b/apps/essimporter/importnpcc.hpp new file mode 100644 index 000000000..a23ab1e50 --- /dev/null +++ b/apps/essimporter/importnpcc.hpp @@ -0,0 +1,37 @@ +#ifndef OPENMW_ESSIMPORT_NPCC_H +#define OPENMW_ESSIMPORT_NPCC_H + +#include + +#include + +#include "importinventory.hpp" + +namespace ESM +{ + class ESMReader; +} + +namespace ESSImport +{ + + struct NPCC + { + struct NPDT + { + unsigned char mDisposition; + unsigned char unknown; + unsigned char mReputation; + unsigned char unknown2; + int mIndex; + } mNPDT; + + Inventory mInventory; + ESM::AIPackageList mAiPackages; + + void load(ESM::ESMReader &esm); + }; + +} + +#endif diff --git a/apps/essimporter/importplayer.cpp b/apps/essimporter/importplayer.cpp new file mode 100644 index 000000000..9845ab072 --- /dev/null +++ b/apps/essimporter/importplayer.cpp @@ -0,0 +1,85 @@ +#include "importplayer.hpp" + +#include + +namespace ESSImport +{ + + void REFR::load(ESM::ESMReader &esm) + { + esm.getHNT(mRefNum.mIndex, "FRMR"); + + mRefID = esm.getHNString("NAME"); + + mActorData.load(esm); + + esm.getHNOT(mPos, "DATA", 24); + } + + void PCDT::load(ESM::ESMReader &esm) + { + while (esm.isNextSub("DNAM")) + { + mKnownDialogueTopics.push_back(esm.getHString()); + } + + if (esm.isNextSub("MNAM")) + esm.skipHSub(); // If this field is here it seems to specify the interior cell the player is in, + // but it's not always here, so it's kinda useless + + esm.getHNT(mPNAM, "PNAM"); + + if (esm.isNextSub("SNAM")) + esm.skipHSub(); + if (esm.isNextSub("NAM9")) + esm.skipHSub(); + + mBounty = 0; + esm.getHNOT(mBounty, "CNAM"); + + mBirthsign = esm.getHNOString("BNAM"); + + // Holds the names of the last used Alchemy apparatus. Don't need to import this ATM, + // because our GUI auto-selects the best apparatus. + if (esm.isNextSub("NAM0")) + esm.skipHSub(); + if (esm.isNextSub("NAM1")) + esm.skipHSub(); + if (esm.isNextSub("NAM2")) + esm.skipHSub(); + if (esm.isNextSub("NAM3")) + esm.skipHSub(); + + if (esm.isNextSub("ENAM")) + esm.skipHSub(); + + if (esm.isNextSub("LNAM")) + esm.skipHSub(); + + while (esm.isNextSub("FNAM")) + { + FNAM fnam; + esm.getHT(fnam); + mFactions.push_back(fnam); + } + + if (esm.isNextSub("AADT")) + esm.skipHSub(); // 44 bytes, no clue + + if (esm.isNextSub("KNAM")) + esm.skipHSub(); // assigned Quick Keys, I think + + if (esm.isNextSub("WERE")) + { + // some werewolf data, 152 bytes + // maybe current skills and attributes for werewolf form + esm.getSubHeader(); + esm.skip(152); + } + + // unsure if before or after WERE + if (esm.isNextSub("ANIS")) + esm.skipHSub(); + } + +} diff --git a/apps/essimporter/importplayer.hpp b/apps/essimporter/importplayer.hpp new file mode 100644 index 000000000..bc6b94be2 --- /dev/null +++ b/apps/essimporter/importplayer.hpp @@ -0,0 +1,83 @@ +#ifndef OPENMW_ESSIMPORT_PLAYER_H +#define OPENMW_ESSIMPORT_PLAYER_H + +#include +#include + +#include +#include +#include + +#include "importacdt.hpp" + +namespace ESM +{ + class ESMReader; +} + +namespace ESSImport +{ + +/// Player-agnostic player data +struct REFR +{ + ActorData mActorData; + + std::string mRefID; + ESM::Position mPos; + ESM::RefNum mRefNum; + + void load(ESM::ESMReader& esm); +}; + +/// Other player data +struct PCDT +{ + int mBounty; + std::string mBirthsign; + + std::vector mKnownDialogueTopics; + + enum DrawState_ + { + DrawState_Weapon = 0x80, + DrawState_Spell = 0x100 + }; + enum CameraState + { + CameraState_FirstPerson = 0x8, + CameraState_ThirdPerson = 0xa + }; + +#pragma pack(push) +#pragma pack(1) + struct FNAM + { + unsigned char mRank; + unsigned char mUnknown1[3]; + int mReputation; + unsigned char mFlags; // 0x1: unknown, 0x2: expelled + unsigned char mUnknown2[3]; + ESM::NAME32 mFactionName; + }; + + struct PNAM + { + short mDrawState; // DrawState + short mCameraState; // CameraState + unsigned int mLevelProgress; + float mSkillProgress[27]; // skill progress, non-uniform scaled + unsigned char mSkillIncreases[8]; // number of skill increases for each attribute + unsigned char mUnknown3[88]; + }; +#pragma pack(pop) + + std::vector mFactions; + PNAM mPNAM; + + void load(ESM::ESMReader& esm); +}; + +} + +#endif diff --git a/apps/essimporter/importques.cpp b/apps/essimporter/importques.cpp new file mode 100644 index 000000000..78b779e43 --- /dev/null +++ b/apps/essimporter/importques.cpp @@ -0,0 +1,14 @@ +#include "importques.hpp" + +#include + +namespace ESSImport +{ + + void QUES::load(ESM::ESMReader &esm) + { + while (esm.isNextSub("DATA")) + mInfo.push_back(esm.getHString()); + } + +} diff --git a/apps/essimporter/importques.hpp b/apps/essimporter/importques.hpp new file mode 100644 index 000000000..51fe22434 --- /dev/null +++ b/apps/essimporter/importques.hpp @@ -0,0 +1,28 @@ +#ifndef OPENMW_ESSIMPORT_IMPORTQUES_H +#define OPENMW_ESSIMPORT_IMPORTQUES_H + +#include +#include + +namespace ESM +{ + class ESMReader; +} + +namespace ESSImport +{ + + /// State for a quest + /// Presumably this record only exists when Tribunal is installed, + /// since pre-Tribunal there weren't any quest names in the data files. + struct QUES + { + std::string mName; // NAME, should be assigned from outside as usual + std::vector mInfo; // list of journal entries for the quest + + void load(ESM::ESMReader& esm); + }; + +} + +#endif diff --git a/apps/essimporter/importscpt.cpp b/apps/essimporter/importscpt.cpp new file mode 100644 index 000000000..652383cda --- /dev/null +++ b/apps/essimporter/importscpt.cpp @@ -0,0 +1,26 @@ +#include "importscpt.hpp" + +#include + + + +namespace ESSImport +{ + + void SCPT::load(ESM::ESMReader &esm) + { + esm.getHNT(mSCHD, "SCHD"); + + mSCRI.load(esm); + + mRefNum = -1; + if (esm.isNextSub("RNAM")) + { + mRunning = true; + esm.getHT(mRefNum); + } + else + mRunning = false; + } + +} diff --git a/apps/essimporter/importscpt.hpp b/apps/essimporter/importscpt.hpp new file mode 100644 index 000000000..ce54c3a73 --- /dev/null +++ b/apps/essimporter/importscpt.hpp @@ -0,0 +1,32 @@ +#ifndef OPENMW_ESSIMPORT_IMPORTSCPT_H +#define OPENMW_ESSIMPORT_IMPORTSCPT_H + +#include "importscri.hpp" + +#include + +namespace ESM +{ + class ESMReader; +} + +namespace ESSImport +{ + + // A running global script + struct SCPT + { + ESM::Script::SCHD mSCHD; + + // values of local variables + SCRI mSCRI; + + bool mRunning; + int mRefNum; // Targeted reference, -1: no reference + + void load(ESM::ESMReader& esm); + }; + +} + +#endif diff --git a/apps/essimporter/importscri.cpp b/apps/essimporter/importscri.cpp new file mode 100644 index 000000000..de0b35c86 --- /dev/null +++ b/apps/essimporter/importscri.cpp @@ -0,0 +1,55 @@ +#include "importscri.hpp" + +#include + +namespace ESSImport +{ + + void SCRI::load(ESM::ESMReader &esm) + { + mScript = esm.getHNOString("SCRI"); + + int numShorts = 0, numLongs = 0, numFloats = 0; + if (esm.isNextSub("SLCS")) + { + esm.getSubHeader(); + esm.getT(numShorts); + esm.getT(numLongs); + esm.getT(numFloats); + } + + if (esm.isNextSub("SLSD")) + { + esm.getSubHeader(); + for (int i=0; i + +#include + +namespace ESM +{ + class ESMReader; +} + +namespace ESSImport +{ + + /// Local variable assigments for a running script + struct SCRI + { + std::string mScript; + + std::vector mShorts; + std::vector mLongs; + std::vector mFloats; + + void load(ESM::ESMReader& esm); + }; + +} + +#endif diff --git a/apps/essimporter/main.cpp b/apps/essimporter/main.cpp new file mode 100644 index 000000000..a4ad114ec --- /dev/null +++ b/apps/essimporter/main.cpp @@ -0,0 +1,77 @@ +#include + +#include +#include +#include +#include + +#include + +#include "importer.hpp" + +namespace bpo = boost::program_options; +namespace bfs = boost::filesystem; + + + +int main(int argc, char** argv) +{ + try + { + bpo::options_description desc("Syntax: openmw-essimporter infile.ess outfile.omwsave\nAllowed options"); + bpo::positional_options_description p_desc; + desc.add_options() + ("help,h", "produce help message") + ("mwsave,m", bpo::value(), "morrowind .ess save file") + ("output,o", bpo::value(), "output file (.omwsave)") + ("compare,c", "compare two .ess files") + ("encoding", boost::program_options::value()->default_value("win1252"), "encoding of the save file") + ; + p_desc.add("mwsave", 1).add("output", 1); + + bpo::variables_map variables; + + bpo::parsed_options parsed = bpo::command_line_parser(argc, argv) + .options(desc) + .positional(p_desc) + .run(); + + bpo::store(parsed, variables); + + if(variables.count("help") || !variables.count("mwsave") || !variables.count("output")) { + std::cout << desc; + return 0; + } + + bpo::notify(variables); + + Files::ConfigurationManager cfgManager(true); + cfgManager.readConfiguration(variables, desc); + + std::string essFile = variables["mwsave"].as(); + std::string outputFile = variables["output"].as(); + std::string encoding = variables["encoding"].as(); + + ESSImport::Importer importer(essFile, outputFile, encoding); + + if (variables.count("compare")) + importer.compare(); + else + { + const std::string& ext = ".omwsave"; + if (boost::filesystem::exists(boost::filesystem::path(outputFile)) + && (outputFile.size() < ext.size() || outputFile.substr(outputFile.size()-ext.size()) != ext)) + { + throw std::runtime_error("Output file already exists and does not end in .omwsave. Did you mean to use --compare?"); + } + importer.run(); + } + } + catch (std::exception& e) + { + std::cerr << "Error: " << e.what() << std::endl; + return 1; + } + + return 0; +} diff --git a/apps/launcher/CMakeLists.txt b/apps/launcher/CMakeLists.txt index d7733ba0e..0de79f8f6 100644 --- a/apps/launcher/CMakeLists.txt +++ b/apps/launcher/CMakeLists.txt @@ -5,21 +5,16 @@ set(LAUNCHER maindialog.cpp playpage.cpp textslotmsgbox.cpp + settingspage.cpp - settings/gamesettings.cpp settings/graphicssettings.cpp - settings/launchersettings.cpp - utils/checkablemessagebox.cpp utils/profilescombobox.cpp utils/textinputdialog.cpp utils/lineedit.cpp - ${CMAKE_SOURCE_DIR}/files/launcher/launcher.rc + ${CMAKE_SOURCE_DIR}/files/windows/launcher.rc ) -if(NOT WIN32) - LIST(APPEND LAUNCHER unshieldthread.cpp) -endif(NOT WIN32) set(LAUNCHER_HEADER datafilespage.hpp @@ -27,21 +22,14 @@ set(LAUNCHER_HEADER maindialog.hpp playpage.hpp textslotmsgbox.hpp + settingspage.hpp - settings/gamesettings.hpp settings/graphicssettings.hpp - settings/launchersettings.hpp - settings/settingsbase.hpp - utils/checkablemessagebox.hpp utils/profilescombobox.hpp utils/textinputdialog.hpp utils/lineedit.hpp ) -if(NOT WIN32) - LIST(APPEND LAUNCHER_HEADER unshieldthread.hpp) -endif(NOT WIN32) - # Headers that must be pre-processed set(LAUNCHER_HEADER_MOC @@ -50,25 +38,21 @@ set(LAUNCHER_HEADER_MOC maindialog.hpp playpage.hpp textslotmsgbox.hpp + settingspage.hpp utils/textinputdialog.hpp - utils/checkablemessagebox.hpp utils/profilescombobox.hpp utils/lineedit.hpp ) -if(NOT WIN32) - LIST(APPEND LAUNCHER_HEADER_MOC unshieldthread.hpp) -endif(NOT WIN32) - - set(LAUNCHER_UI ${CMAKE_SOURCE_DIR}/files/ui/datafilespage.ui ${CMAKE_SOURCE_DIR}/files/ui/graphicspage.ui ${CMAKE_SOURCE_DIR}/files/ui/mainwindow.ui ${CMAKE_SOURCE_DIR}/files/ui/playpage.ui ${CMAKE_SOURCE_DIR}/files/ui/contentselector.ui + ${CMAKE_SOURCE_DIR}/files/ui/settingspage.ui ) source_group(launcher FILES ${LAUNCHER} ${LAUNCHER_HEADER}) @@ -90,11 +74,11 @@ QT4_WRAP_UI(UI_HDRS ${LAUNCHER_UI}) include(${QT_USE_FILE}) include_directories(${CMAKE_CURRENT_BINARY_DIR}) if(NOT WIN32) - include_directories(${LIBUNSHIELD_INCLUDE}) + include_directories(${LIBUNSHIELD_INCLUDE_DIR}) endif(NOT WIN32) # Main executable -add_executable(omwlauncher +add_executable(openmw-launcher ${GUI_TYPE} ${LAUNCHER} ${LAUNCHER_HEADER} @@ -103,7 +87,7 @@ add_executable(omwlauncher ${UI_HDRS} ) -target_link_libraries(omwlauncher +target_link_libraries(openmw-launcher ${Boost_LIBRARIES} ${OGRE_LIBRARIES} ${OGRE_STATIC_PLUGINS} @@ -111,16 +95,10 @@ target_link_libraries(omwlauncher ${QT_LIBRARIES} components ) -if(NOT WIN32) - target_link_libraries(omwlauncher - ${LIBUNSHIELD_LIBRARY} - ) -endif(NOT WIN32) - if (BUILD_WITH_CODE_COVERAGE) add_definitions (--coverage) - target_link_libraries(omwlauncher gcov) + target_link_libraries(openmw-launcher gcov) endif() diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index 362d7562c..7861894b0 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -1,5 +1,7 @@ #include "datafilespage.hpp" +#include + #include #include #include @@ -9,18 +11,19 @@ #include #include - #include +#include + +#include +#include #include "utils/textinputdialog.hpp" #include "utils/profilescombobox.hpp" -#include "settings/gamesettings.hpp" -#include "settings/launchersettings.hpp" -#include "components/contentselector/view/contentselector.hpp" +const char *Launcher::DataFilesPage::mDefaultContentListName = "Default"; -Launcher::DataFilesPage::DataFilesPage(Files::ConfigurationManager &cfg, GameSettings &gameSettings, LauncherSettings &launcherSettings, QWidget *parent) +Launcher::DataFilesPage::DataFilesPage(Files::ConfigurationManager &cfg, Config::GameSettings &gameSettings, Config::LauncherSettings &launcherSettings, QWidget *parent) : mCfgMgr(cfg) , mGameSettings(gameSettings) , mLauncherSettings(launcherSettings) @@ -30,31 +33,92 @@ Launcher::DataFilesPage::DataFilesPage(Files::ConfigurationManager &cfg, GameSet setObjectName ("DataFilesPage"); mSelector = new ContentSelectorView::ContentSelector (ui.contentSelectorWidget); + mProfileDialog = new TextInputDialog(tr("New Content List"), tr("Content List name:"), this); + + connect(mProfileDialog->lineEdit(), SIGNAL(textChanged(QString)), + this, SLOT(updateOkButton(QString))); + buildView(); - setupDataFiles(); + loadSettings(); +} + +void Launcher::DataFilesPage::buildView() +{ + ui.verticalLayout->insertWidget (0, mSelector->uiWidget()); + + //tool buttons + ui.newProfileButton->setToolTip ("Create a new Content List"); + ui.deleteProfileButton->setToolTip ("Delete an existing Content List"); + + //combo box + ui.profilesComboBox->addItem(mDefaultContentListName); + ui.profilesComboBox->setPlaceholderText (QString("Select a Content List...")); + ui.profilesComboBox->setCurrentIndex(ui.profilesComboBox->findText(QLatin1String(mDefaultContentListName))); + + // Add the actions to the toolbuttons + ui.newProfileButton->setDefaultAction (ui.newProfileAction); + ui.deleteProfileButton->setDefaultAction (ui.deleteProfileAction); + + //establish connections + connect (ui.profilesComboBox, SIGNAL (currentIndexChanged(int)), + this, SLOT (slotProfileChanged(int))); + + connect (ui.profilesComboBox, SIGNAL (profileRenamed(QString, QString)), + this, SLOT (slotProfileRenamed(QString, QString))); + + connect (ui.profilesComboBox, SIGNAL (signalProfileChanged(QString, QString)), + this, SLOT (slotProfileChangedByUser(QString, QString))); } -void Launcher::DataFilesPage::loadSettings() +bool Launcher::DataFilesPage::loadSettings() +{ + QStringList profiles = mLauncherSettings.getContentLists(); + QString currentProfile = mLauncherSettings.getCurrentContentListName(); + + qDebug() << "current profile is: " << currentProfile; + + foreach (const QString &item, profiles) + addProfile (item, false); + + // Hack: also add the current profile + if (!currentProfile.isEmpty()) + addProfile(currentProfile, true); + + return true; +} + +void Launcher::DataFilesPage::populateFileViews(const QString& contentModelName) { QStringList paths = mGameSettings.getDataDirs(); - paths.insert (0, mDataLocal); - PathIterator pathIterator (paths); - QString profileName = ui.profilesComboBox->currentText(); + foreach(const QString &path, paths) + mSelector->addFiles(path); + + mDataLocal = mGameSettings.getDataLocal(); + + if (!mDataLocal.isEmpty()) + mSelector->addFiles(mDataLocal); + + paths.insert(0, mDataLocal); + PathIterator pathIterator(paths); - QStringList files = mLauncherSettings.values(QString("Profiles/") + profileName, Qt::MatchExactly); + mSelector->setProfileContent(filesInProfile(contentModelName, pathIterator)); +} +QStringList Launcher::DataFilesPage::filesInProfile(const QString& profileName, PathIterator& pathIterator) +{ + QStringList files = mLauncherSettings.getContentListFiles(profileName); QStringList filepaths; - foreach (const QString &file, files) + foreach(const QString& file, files) { - QString filepath = pathIterator.findFirstPath (file); + QString filepath = pathIterator.findFirstPath(file); if (!filepath.isEmpty()) filepaths << filepath; } - mSelector->setProfileContent (filepaths); + return filepaths; } void Launcher::DataFilesPage::saveSettings(const QString &profile) @@ -67,50 +131,20 @@ void Launcher::DataFilesPage::saveSettings(const QString &profile) //retrieve the files selected for the profile ContentSelectorModel::ContentFileList items = mSelector->selectedFiles(); - removeProfile (profileName); - - mGameSettings.remove(QString("content")); - //set the value of the current profile (not necessarily the profile being saved!) - mLauncherSettings.setValue(QString("Profiles/currentprofile"), ui.profilesComboBox->currentText()); + mLauncherSettings.setCurrentContentListName(ui.profilesComboBox->currentText()); + QStringList fileNames; foreach(const ContentSelectorModel::EsmFile *item, items) { - mLauncherSettings.setMultiValue(QString("Profiles/") + profileName, item->fileName()); - mGameSettings.setMultiValue(QString("content"), item->fileName()); + fileNames.append(item->fileName()); } - -} - -void Launcher::DataFilesPage::buildView() -{ - ui.verticalLayout->insertWidget (0, mSelector->uiWidget()); - - //tool buttons - ui.newProfileButton->setToolTip ("Create a new profile"); - ui.deleteProfileButton->setToolTip ("Delete an existing profile"); - - //combo box - ui.profilesComboBox->addItem ("Default"); - ui.profilesComboBox->setPlaceholderText (QString("Select a profile...")); - - // Add the actions to the toolbuttons - ui.newProfileButton->setDefaultAction (ui.newProfileAction); - ui.deleteProfileButton->setDefaultAction (ui.deleteProfileAction); - - //establish connections - connect (ui.profilesComboBox, SIGNAL (currentIndexChanged(int)), - this, SLOT (slotProfileChanged(int))); - - connect (ui.profilesComboBox, SIGNAL (profileRenamed(QString, QString)), - this, SLOT (slotProfileRenamed(QString, QString))); - - connect (ui.profilesComboBox, SIGNAL (signalProfileChanged(QString, QString)), - this, SLOT (slotProfileChangedByUser(QString, QString))); + mLauncherSettings.setContentList(profileName, fileNames); + mGameSettings.setContentList(fileNames); } void Launcher::DataFilesPage::removeProfile(const QString &profile) { - mLauncherSettings.remove(QString("Profiles/") + profile); + mLauncherSettings.removeContentList(profile); } QAbstractItemModel *Launcher::DataFilesPage::profilesModel() const @@ -127,9 +161,11 @@ void Launcher::DataFilesPage::setProfile(int index, bool savePrevious) { if (index >= -1 && index < ui.profilesComboBox->count()) { - QString previous = ui.profilesComboBox->itemText(ui.profilesComboBox->currentIndex()); + QString previous = mPreviousProfile; QString current = ui.profilesComboBox->itemText(index); + mPreviousProfile = current; + setProfile (previous, current, savePrevious); } } @@ -145,7 +181,7 @@ void Launcher::DataFilesPage::setProfile (const QString &previous, const QString ui.profilesComboBox->setCurrentProfile (ui.profilesComboBox->findText (current)); - loadSettings(); + populateFileViews(current); checkForDefaultProfile(); } @@ -177,68 +213,28 @@ void Launcher::DataFilesPage::slotProfileRenamed(const QString &previous, const void Launcher::DataFilesPage::slotProfileChanged(int index) { - setProfile (index, true); -} - -void Launcher::DataFilesPage::setupDataFiles() -{ - QStringList paths = mGameSettings.getDataDirs(); - - foreach (const QString &path, paths) - mSelector->addFiles(path); + // in case the event was triggered externally + if (ui.profilesComboBox->currentIndex() != index) + ui.profilesComboBox->setCurrentIndex(index); - mDataLocal = mGameSettings.getDataLocal(); - - if (!mDataLocal.isEmpty()) - mSelector->addFiles(mDataLocal); - - QStringList profiles; - QString currentProfile = mLauncherSettings.getSettings().value("Profiles/currentprofile"); - - foreach (QString key, mLauncherSettings.getSettings().keys()) - { - if (key.contains("Profiles/")) - { - QString profile = key.mid (9); - if (profile != "currentprofile") - { - if (!profiles.contains(profile)) - profiles << profile; - } - } - } - - foreach (const QString &item, profiles) - addProfile (item, false); - - setProfile (ui.profilesComboBox->findText(currentProfile), false); - - loadSettings(); + setProfile (index, true); } void Launcher::DataFilesPage::on_newProfileAction_triggered() { - TextInputDialog newDialog (tr("New Profile"), tr("Profile name:"), this); - - if (newDialog.exec() != QDialog::Accepted) + if (mProfileDialog->exec() != QDialog::Accepted) return; - QString profile = newDialog.getText(); + QString profile = mProfileDialog->lineEdit()->text(); if (profile.isEmpty()) - return; + return; saveSettings(); - mSelector->clearCheckStates(); + mLauncherSettings.setCurrentContentListName(profile); addProfile(profile, true); - - mSelector->setGameFile(); - - saveSettings(); - - emit signalProfileChanged (ui.profilesComboBox->findText(profile)); } void Launcher::DataFilesPage::addProfile (const QString &profile, bool setAsCurrent) @@ -246,10 +242,8 @@ void Launcher::DataFilesPage::addProfile (const QString &profile, bool setAsCurr if (profile.isEmpty()) return; - if (ui.profilesComboBox->findText (profile) != -1) - return; - - ui.profilesComboBox->addItem (profile); + if (ui.profilesComboBox->findText (profile) == -1) + ui.profilesComboBox->addItem (profile); if (setAsCurrent) setProfile (ui.profilesComboBox->findText (profile), false); @@ -265,22 +259,35 @@ void Launcher::DataFilesPage::on_deleteProfileAction_triggered() if (!showDeleteMessageBox (profile)) return; - // Remove the profile from the combobox - ui.profilesComboBox->removeItem (ui.profilesComboBox->findText (profile)); + // this should work since the Default profile can't be deleted and is always index 0 + int next = ui.profilesComboBox->currentIndex()-1; + + // changing the profile forces a reload of plugin file views. + ui.profilesComboBox->setCurrentIndex(next); removeProfile(profile); + ui.profilesComboBox->removeItem(ui.profilesComboBox->findText(profile)); - saveSettings(); + checkForDefaultProfile(); +} - loadSettings(); +void Launcher::DataFilesPage::updateOkButton(const QString &text) +{ + // We do this here because we need the profiles combobox text + if (text.isEmpty()) { + mProfileDialog->setOkButtonEnabled(false); + return; + } - checkForDefaultProfile(); + (ui.profilesComboBox->findText(text) == -1) + ? mProfileDialog->setOkButtonEnabled(true) + : mProfileDialog->setOkButtonEnabled(false); } void Launcher::DataFilesPage::checkForDefaultProfile() { //don't allow deleting "Default" profile - bool success = (ui.profilesComboBox->currentText() != "Default"); + bool success = (ui.profilesComboBox->currentText() != mDefaultContentListName); ui.deleteProfileAction->setEnabled (success); ui.profilesComboBox->setEditEnabled (success); @@ -289,10 +296,10 @@ void Launcher::DataFilesPage::checkForDefaultProfile() bool Launcher::DataFilesPage::showDeleteMessageBox (const QString &text) { QMessageBox msgBox(this); - msgBox.setWindowTitle(tr("Delete Profile")); + msgBox.setWindowTitle(tr("Delete Content List")); msgBox.setIcon(QMessageBox::Warning); msgBox.setStandardButtons(QMessageBox::Cancel); - msgBox.setText(tr("Are you sure you want to delete %0?").arg(text)); + msgBox.setText(tr("Are you sure you want to delete %1?").arg(text)); QAbstractButton *deleteButton = msgBox.addButton(tr("Delete"), QMessageBox::ActionRole); diff --git a/apps/launcher/datafilespage.hpp b/apps/launcher/datafilespage.hpp index 37603a210..d25d20fc9 100644 --- a/apps/launcher/datafilespage.hpp +++ b/apps/launcher/datafilespage.hpp @@ -14,12 +14,12 @@ class QMenu; namespace Files { struct ConfigurationManager; } namespace ContentSelectorView { class ContentSelector; } +namespace Config { class GameSettings; + class LauncherSettings; } namespace Launcher { class TextInputDialog; - class GameSettings; - class LauncherSettings; class ProfilesComboBox; class DataFilesPage : public QWidget @@ -30,8 +30,8 @@ namespace Launcher Ui::DataFilesPage ui; public: - explicit DataFilesPage (Files::ConfigurationManager &cfg, GameSettings &gameSettings, - LauncherSettings &launcherSettings, QWidget *parent = 0); + explicit DataFilesPage (Files::ConfigurationManager &cfg, Config::GameSettings &gameSettings, + Config::LauncherSettings &launcherSettings, QWidget *parent = 0); QAbstractItemModel* profilesModel() const; @@ -39,7 +39,7 @@ namespace Launcher //void writeConfig(QString profile = QString()); void saveSettings(const QString &profile = ""); - void loadSettings(); + bool loadSettings(); signals: void signalProfileChanged (int index); @@ -53,24 +53,31 @@ namespace Launcher void slotProfileRenamed(const QString &previous, const QString ¤t); void slotProfileDeleted(const QString &item); + void updateOkButton(const QString &text); + void on_newProfileAction_triggered(); void on_deleteProfileAction_triggered(); + public: + /// Content List that is always present + const static char *mDefaultContentListName; + private: - QMenu *mContextMenu; + TextInputDialog *mProfileDialog; Files::ConfigurationManager &mCfgMgr; - GameSettings &mGameSettings; - LauncherSettings &mLauncherSettings; + Config::GameSettings &mGameSettings; + Config::LauncherSettings &mLauncherSettings; + + QString mPreviousProfile; QString mDataLocal; void setPluginsCheckstates(Qt::CheckState state); void buildView(); - void setupDataFiles(); void setupConfig(); void readConfig(); void setProfile (int index, bool savePrevious); @@ -79,6 +86,7 @@ namespace Launcher bool showDeleteMessageBox (const QString &text); void addProfile (const QString &profile, bool setAsCurrent); void checkForDefaultProfile(); + void populateFileViews(const QString& contentModelName); class PathIterator { @@ -131,6 +139,8 @@ namespace Launcher } }; + + QStringList filesInProfile(const QString& profileName, PathIterator& pathIterator); }; } #endif diff --git a/apps/launcher/graphicspage.cpp b/apps/launcher/graphicspage.cpp index 638237f34..cdb51348c 100644 --- a/apps/launcher/graphicspage.cpp +++ b/apps/launcher/graphicspage.cpp @@ -10,7 +10,10 @@ #define MAC_OS_X_VERSION_MIN_REQUIRED __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ #endif // MAC_OS_X_VERSION_MIN_REQUIRED -#include +#include + +#include +#include #include @@ -33,7 +36,11 @@ QString getAspect(int x, int y) } Launcher::GraphicsPage::GraphicsPage(Files::ConfigurationManager &cfg, GraphicsSettings &graphicsSetting, QWidget *parent) - : mCfgMgr(cfg) + : mOgre(NULL) + , mSelectedRenderSystem(NULL) + , mOpenGLRenderSystem(NULL) + , mDirect3DRenderSystem(NULL) + , mCfgMgr(cfg) , mGraphicsSettings(graphicsSetting) , QWidget(parent) { @@ -60,7 +67,7 @@ bool Launcher::GraphicsPage::setupOgre() } catch(Ogre::Exception &ex) { - QString ogreError = QString::fromStdString(ex.getFullDescription().c_str()); + QString ogreError = QString::fromUtf8(ex.getFullDescription().c_str()); QMessageBox msgBox; msgBox.setWindowTitle("Error creating Ogre::Root"); msgBox.setIcon(QMessageBox::Critical); @@ -128,11 +135,12 @@ bool Launcher::GraphicsPage::setupSDL() msgBox.setWindowTitle(tr("Error receiving number of screens")); msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(tr("
SDL_GetNumDisplayModes failed:

") + QString::fromStdString(SDL_GetError()) + "
"); + msgBox.setText(tr("
SDL_GetNumDisplayModes failed:

") + QString::fromUtf8(SDL_GetError()) + "
"); msgBox.exec(); return false; } + screenComboBox->clear(); for (int i = 0; i < displays; i++) { screenComboBox->addItem(QString(tr("Screen ")) + QString::number(i + 1)); @@ -145,7 +153,7 @@ bool Launcher::GraphicsPage::loadSettings() { if (!setupSDL()) return false; - if (!setupOgre()) + if (!mOgre && !setupOgre()) return false; if (mGraphicsSettings.value(QString("Video/vsync")) == QLatin1String("true")) @@ -154,6 +162,9 @@ bool Launcher::GraphicsPage::loadSettings() if (mGraphicsSettings.value(QString("Video/fullscreen")) == QLatin1String("true")) fullScreenCheckBox->setCheckState(Qt::Checked); + if (mGraphicsSettings.value(QString("Video/window border")) == QLatin1String("true")) + windowBorderCheckBox->setCheckState(Qt::Checked); + int aaIndex = antiAliasingComboBox->findText(mGraphicsSettings.value(QString("Video/antialiasing"))); if (aaIndex != -1) antiAliasingComboBox->setCurrentIndex(aaIndex); @@ -188,6 +199,9 @@ void Launcher::GraphicsPage::saveSettings() fullScreenCheckBox->checkState() ? mGraphicsSettings.setValue(QString("Video/fullscreen"), QString("true")) : mGraphicsSettings.setValue(QString("Video/fullscreen"), QString("false")); + windowBorderCheckBox->checkState() ? mGraphicsSettings.setValue(QString("Video/window border"), QString("true")) + : mGraphicsSettings.setValue(QString("Video/window border"), QString("false")); + mGraphicsSettings.setValue(QString("Video/antialiasing"), antiAliasingComboBox->currentText()); mGraphicsSettings.setValue(QString("Video/render system"), rendererComboBox->currentText()); @@ -223,7 +237,7 @@ QStringList Launcher::GraphicsPage::getAvailableOptions(const QString &key, Ogre opt_it != i->second.possibleValues.end(); ++opt_it, ++idx) { if (strcmp (key.toStdString().c_str(), i->first.c_str()) == 0) { - result << ((key == "FSAA") ? QString("MSAA ") : QString("")) + QString::fromStdString((*opt_it).c_str()).simplified(); + result << ((key == "FSAA") ? QString("MSAA ") : QString("")) + QString::fromUtf8((*opt_it).c_str()).simplified(); } } } @@ -252,7 +266,7 @@ QStringList Launcher::GraphicsPage::getAvailableResolutions(int screen) msgBox.setWindowTitle(tr("Error receiving resolutions")); msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(tr("
SDL_GetNumDisplayModes failed:

") + QString::fromStdString(SDL_GetError()) + "
"); + msgBox.setText(tr("
SDL_GetNumDisplayModes failed:

") + QString::fromUtf8(SDL_GetError()) + "
"); msgBox.exec(); return result; } @@ -265,7 +279,7 @@ QStringList Launcher::GraphicsPage::getAvailableResolutions(int screen) msgBox.setWindowTitle(tr("Error receiving resolutions")); msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(tr("
SDL_GetDisplayMode failed:

") + QString::fromStdString(SDL_GetError()) + "
"); + msgBox.setText(tr("
SDL_GetDisplayMode failed:

") + QString::fromUtf8(SDL_GetError()) + "
"); msgBox.exec(); return result; } @@ -326,10 +340,12 @@ void Launcher::GraphicsPage::slotFullScreenChanged(int state) customRadioButton->setEnabled(false); customWidthSpinBox->setEnabled(false); customHeightSpinBox->setEnabled(false); + windowBorderCheckBox->setEnabled(false); } else { customRadioButton->setEnabled(true); customWidthSpinBox->setEnabled(true); customHeightSpinBox->setEnabled(true); + windowBorderCheckBox->setEnabled(true); } } diff --git a/apps/launcher/graphicspage.hpp b/apps/launcher/graphicspage.hpp index da4cb9fb3..213b6bccb 100644 --- a/apps/launcher/graphicspage.hpp +++ b/apps/launcher/graphicspage.hpp @@ -3,14 +3,11 @@ #include -#include -#include - #include - #include "ui_graphicspage.h" +namespace Ogre { class Root; class RenderSystem; } namespace Files { struct ConfigurationManager; } diff --git a/apps/launcher/main.cpp b/apps/launcher/main.cpp index fabf77d90..11ea56869 100644 --- a/apps/launcher/main.cpp +++ b/apps/launcher/main.cpp @@ -1,3 +1,6 @@ +#include +#include + #include #include #include @@ -15,50 +18,63 @@ int main(int argc, char *argv[]) { - SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software"); - SDL_SetMainReady(); - if (SDL_Init(SDL_INIT_VIDEO) != 0) + try { - qDebug() << "SDL_Init failed: " << QString::fromStdString(SDL_GetError()); - return 0; - } + SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software"); + SDL_SetMainReady(); + if (SDL_Init(SDL_INIT_VIDEO) != 0) + { + qDebug() << "SDL_Init failed: " << QString::fromUtf8(SDL_GetError()); + return 0; + } + signal(SIGINT, SIG_DFL); // We don't want to use the SDL event loop in the launcher, + // so reset SIGINT which SDL wants to redirect to an SDL_Quit event. - QApplication app(argc, argv); + QApplication app(argc, argv); - // Now we make sure the current dir is set to application path - QDir dir(QCoreApplication::applicationDirPath()); + // Now we make sure the current dir is set to application path + QDir dir(QCoreApplication::applicationDirPath()); - #ifdef Q_OS_MAC - if (dir.dirName() == "MacOS") { - dir.cdUp(); - dir.cdUp(); - dir.cdUp(); - } + #ifdef Q_OS_MAC + if (dir.dirName() == "MacOS") { + dir.cdUp(); + dir.cdUp(); + dir.cdUp(); + } + + // force Qt to load only LOCAL plugins, don't touch system Qt installation + QDir pluginsPath(QCoreApplication::applicationDirPath()); + pluginsPath.cdUp(); + pluginsPath.cd("Plugins"); - // force Qt to load only LOCAL plugins, don't touch system Qt installation - QDir pluginsPath(QCoreApplication::applicationDirPath()); - pluginsPath.cdUp(); - pluginsPath.cd("Plugins"); + QStringList libraryPaths; + libraryPaths << pluginsPath.path() << QCoreApplication::applicationDirPath(); + app.setLibraryPaths(libraryPaths); + #endif - QStringList libraryPaths; - libraryPaths << pluginsPath.path() << QCoreApplication::applicationDirPath(); - app.setLibraryPaths(libraryPaths); - #endif + QDir::setCurrent(dir.absolutePath()); - QDir::setCurrent(dir.absolutePath()); + // Support non-latin characters + QTextCodec::setCodecForCStrings(QTextCodec::codecForName("UTF-8")); - // Support non-latin characters - QTextCodec::setCodecForCStrings(QTextCodec::codecForName("UTF-8")); + Launcher::MainDialog mainWin; - Launcher::MainDialog mainWin; + if (!mainWin.showFirstRunDialog()) + return 0; + + // if (!mainWin.setup()) { + // return 0; + // } - if (mainWin.setup()) { mainWin.show(); - } else { + + int returnValue = app.exec(); + SDL_Quit(); + return returnValue; + } + catch (std::exception& e) + { + std::cerr << "ERROR: " << e.what() << std::endl; return 0; } - - int returnValue = app.exec(); - SDL_Quit(); - return returnValue; } diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index 41d662d30..4c142231d 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -5,53 +5,38 @@ #include #include #include +#include #include #include #include #include #include #include -#include #include #include #include -#ifndef WIN32 - #include "unshieldthread.hpp" -#endif - -#include "textslotmsgbox.hpp" - -#include "utils/checkablemessagebox.hpp" - #include "playpage.hpp" #include "graphicspage.hpp" #include "datafilespage.hpp" +#include "settingspage.hpp" + +using namespace Process; Launcher::MainDialog::MainDialog(QWidget *parent) : mGameSettings(mCfgMgr), QMainWindow (parent) { - // Install the stylesheet font - QFile file; - QFontDatabase fontDatabase; - - const QStringList fonts = fontDatabase.families(); - - // Check if the font is installed - if (!fonts.contains("EB Garamond")) { - - QString font = QString::fromUtf8(mCfgMgr.getGlobalDataPath().string().c_str()) + QString("resources/mygui/EBGaramond-Regular.ttf"); - file.setFileName(font); + setupUi(this); - if (!file.exists()) { - font = QString::fromUtf8(mCfgMgr.getLocalPath().string().c_str()) + QString("resources/mygui/EBGaramond-Regular.ttf"); - } + mGameInvoker = new ProcessInvoker(); + mWizardInvoker = new ProcessInvoker(); - fontDatabase.addApplicationFont(font); - } + connect(mWizardInvoker->getProcess(), SIGNAL(started()), + this, SLOT(wizardStarted())); - setupUi(this); + connect(mWizardInvoker->getProcess(), SIGNAL(finished(int,QProcess::ExitStatus)), + this, SLOT(wizardFinished(int,QProcess::ExitStatus))); iconWidget->setViewMode(QListView::IconMode); iconWidget->setWrapping(false); @@ -76,16 +61,17 @@ Launcher::MainDialog::MainDialog(QWidget *parent) QString revision(OPENMW_VERSION_COMMITHASH); QString tag(OPENMW_VERSION_TAGHASH); + versionLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); if (!revision.isEmpty() && !tag.isEmpty()) { if (revision == tag) { - versionLabel->setText(tr("OpenMW %0 release").arg(OPENMW_VERSION)); + versionLabel->setText(tr("OpenMW %1 release").arg(OPENMW_VERSION)); } else { - versionLabel->setText(tr("OpenMW development (%0)").arg(revision.left(10))); + versionLabel->setText(tr("OpenMW development (%1)").arg(revision.left(10))); } // Add the compile date and time - versionLabel->setToolTip(tr("Compiled on %0 %1").arg(QLocale(QLocale::C).toDate(QString(__DATE__).simplified(), + versionLabel->setToolTip(tr("Compiled on %1 %2").arg(QLocale(QLocale::C).toDate(QString(__DATE__).simplified(), QLatin1String("MMM d yyyy")).toString(Qt::SystemLocaleLongDate), QLocale(QLocale::C).toTime(QString(__TIME__).simplified(), QLatin1String("hh:mm:ss")).toString(Qt::SystemLocaleShortDate))); @@ -94,32 +80,41 @@ Launcher::MainDialog::MainDialog(QWidget *parent) createIcons(); } +Launcher::MainDialog::~MainDialog() +{ + delete mGameInvoker; + delete mWizardInvoker; +} + void Launcher::MainDialog::createIcons() { if (!QIcon::hasThemeIcon("document-new")) QIcon::setThemeName("tango"); - // We create a fallback icon because the default fallback doesn't work - QIcon graphicsIcon = QIcon(":/icons/tango/video-display.png"); - QListWidgetItem *playButton = new QListWidgetItem(iconWidget); playButton->setIcon(QIcon(":/images/openmw.png")); playButton->setText(tr("Play")); playButton->setTextAlignment(Qt::AlignCenter); playButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - QListWidgetItem *graphicsButton = new QListWidgetItem(iconWidget); - graphicsButton->setIcon(QIcon::fromTheme("video-display", graphicsIcon)); - graphicsButton->setText(tr("Graphics")); - graphicsButton->setTextAlignment(Qt::AlignHCenter | Qt::AlignBottom | Qt::AlignAbsolute); - graphicsButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - QListWidgetItem *dataFilesButton = new QListWidgetItem(iconWidget); dataFilesButton->setIcon(QIcon(":/images/openmw-plugin.png")); dataFilesButton->setText(tr("Data Files")); dataFilesButton->setTextAlignment(Qt::AlignHCenter | Qt::AlignBottom); dataFilesButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); + QListWidgetItem *graphicsButton = new QListWidgetItem(iconWidget); + graphicsButton->setIcon(QIcon::fromTheme("video-display")); + graphicsButton->setText(tr("Graphics")); + graphicsButton->setTextAlignment(Qt::AlignHCenter | Qt::AlignBottom | Qt::AlignAbsolute); + graphicsButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); + + QListWidgetItem *settingsButton = new QListWidgetItem(iconWidget); + settingsButton->setIcon(QIcon::fromTheme("preferences-system")); + settingsButton->setText(tr("Settings")); + settingsButton->setTextAlignment(Qt::AlignHCenter | Qt::AlignBottom); + settingsButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); + connect(iconWidget, SIGNAL(currentItemChanged(QListWidgetItem*,QListWidgetItem*)), this, SLOT(changePage(QListWidgetItem*,QListWidgetItem*))); @@ -129,8 +124,9 @@ void Launcher::MainDialog::createIcons() void Launcher::MainDialog::createPages() { mPlayPage = new PlayPage(this); - mGraphicsPage = new GraphicsPage(mCfgMgr, mGraphicsSettings, this); mDataFilesPage = new DataFilesPage(mCfgMgr, mGameSettings, mLauncherSettings, this); + mGraphicsPage = new GraphicsPage(mCfgMgr, mGraphicsSettings, this); + mSettingsPage = new SettingsPage(mCfgMgr, mGameSettings, mLauncherSettings, this); // Set the combobox of the play page to imitate the combobox on the datafilespage mPlayPage->setProfilesModel(mDataFilesPage->profilesModel()); @@ -138,8 +134,9 @@ void Launcher::MainDialog::createPages() // Add the pages to the stacked widget pagesWidget->addWidget(mPlayPage); - pagesWidget->addWidget(mGraphicsPage); pagesWidget->addWidget(mDataFilesPage); + pagesWidget->addWidget(mGraphicsPage); + pagesWidget->addWidget(mSettingsPage); // Select the first page iconWidget->setCurrentItem(iconWidget->item(0), QItemSelectionModel::Select); @@ -153,153 +150,65 @@ void Launcher::MainDialog::createPages() bool Launcher::MainDialog::showFirstRunDialog() { - QStringList iniPaths; - - foreach (const QString &path, mGameSettings.getDataDirs()) { - QDir dir(path); - dir.setPath(dir.canonicalPath()); // Resolve symlinks - - if (dir.exists(QString("Morrowind.ini"))) - iniPaths.append(dir.absoluteFilePath(QString("Morrowind.ini"))); - else - { - if (!dir.cdUp()) - continue; // Cannot move from Data Files - - if (dir.exists(QString("Morrowind.ini"))) - iniPaths.append(dir.absoluteFilePath(QString("Morrowind.ini"))); - } - } + if (!setupLauncherSettings()) + return false; - // Ask the user where the Morrowind.ini is - if (iniPaths.empty()) { + if (mLauncherSettings.value(QString("General/firstrun"), QString("true")) == QLatin1String("true")) + { QMessageBox msgBox; - msgBox.setWindowTitle(tr("Error detecting Morrowind configuration")); - msgBox.setIcon(QMessageBox::Warning); - msgBox.setStandardButtons(QMessageBox::Cancel); - msgBox.setText(QObject::tr("
Could not find Morrowind.ini

\ - OpenMW needs to import settings from this file.

\ - Press \"Browse...\" to specify the location manually.
")); - - QAbstractButton *dirSelectButton = - msgBox.addButton(QObject::tr("B&rowse..."), QMessageBox::ActionRole); - - msgBox.exec(); - - QString iniFile; - if (msgBox.clickedButton() == dirSelectButton) { - iniFile = QFileDialog::getOpenFileName( - NULL, - QObject::tr("Select configuration file"), - QDir::currentPath(), - QString(tr("Morrowind configuration file (*.ini)"))); - } - - if (iniFile.isEmpty()) - return false; // Cancel was clicked; - - QFileInfo info(iniFile); - iniPaths.clear(); - iniPaths.append(info.absoluteFilePath()); - } - - CheckableMessageBox msgBox(this); - msgBox.setWindowTitle(tr("Morrowind installation detected")); - - QIcon icon = QApplication::style()->standardIcon(QStyle::SP_MessageBoxQuestion); - int size = QApplication::style()->pixelMetric(QStyle::PM_MessageBoxIconSize); - msgBox.setIconPixmap(icon.pixmap(size, size)); + msgBox.setWindowTitle(tr("First run")); + msgBox.setIcon(QMessageBox::Question); + msgBox.setStandardButtons(QMessageBox::NoButton); + msgBox.setText(tr("

Welcome to OpenMW!

\ +

It is recommended to run the Installation Wizard.

\ +

The Wizard will let you select an existing Morrowind installation, \ + or install Morrowind for OpenMW to use.

")); - QAbstractButton *importerButton = - msgBox.addButton(tr("Import"), QDialogButtonBox::AcceptRole); // ActionRole doesn't work?! - QAbstractButton *skipButton = - msgBox.addButton(tr("Skip"), QDialogButtonBox::RejectRole); + QAbstractButton *wizardButton = + msgBox.addButton(tr("Run &Installation Wizard"), QMessageBox::AcceptRole); // ActionRole doesn't work?! + QAbstractButton *skipButton = + msgBox.addButton(tr("Skip"), QMessageBox::RejectRole); - Q_UNUSED(skipButton); // Surpress compiler unused warning + Q_UNUSED(skipButton); // Surpress compiler unused warning - msgBox.setStandardButtons(QDialogButtonBox::NoButton); - msgBox.setText(tr("
An existing Morrowind configuration was detected
\ -
Would you like to import settings from Morrowind.ini?
\ -
Warning: In most cases OpenMW needs these settings to run properly
")); - msgBox.setCheckBoxText(tr("Include selected masters and plugins (creates a new profile)")); - msgBox.exec(); - - - if (msgBox.clickedButton() == importerButton) { - - if (iniPaths.count() > 1) { - // Multiple Morrowind.ini files found - bool ok; - QString path = QInputDialog::getItem(this, tr("Multiple configurations found"), - tr("
There are multiple Morrowind.ini files found.

\ - Please select the one you wish to import from:"), iniPaths, 0, false, &ok); - if (ok && !path.isEmpty()) { - iniPaths.clear(); - iniPaths.append(path); - } else { - // Cancel was clicked - return false; - } - } - - // Create the file if it doesn't already exist, else the importer will fail - QString path = QString::fromUtf8(mCfgMgr.getUserConfigPath().string().c_str()) + QString("openmw.cfg"); - QFile file(path); + msgBox.exec(); - if (!file.exists()) { - if (!file.open(QIODevice::ReadWrite)) { - // File cannot be created - QMessageBox msgBox; - msgBox.setWindowTitle(tr("Error writing OpenMW configuration file")); - msgBox.setIcon(QMessageBox::Critical); - msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(tr("
Could not open or create %0 for writing

\ - Please make sure you have the right permissions \ - and try again.
").arg(file.fileName())); - msgBox.exec(); + if (msgBox.clickedButton() == wizardButton) + { + if (!mWizardInvoker->startProcess(QLatin1String("openmw-wizard"), false)) { return false; + } else { + return true; } - - file.close(); } + } - // Construct the arguments to run the importer - QStringList arguments; - - if (msgBox.isChecked()) - arguments.append(QString("--game-files")); + return setup(); +} - arguments.append(QString("--encoding")); - arguments.append(mGameSettings.value(QString("encoding"), QString("win1252"))); - arguments.append(QString("--ini")); - arguments.append(iniPaths.first()); - arguments.append(QString("--cfg")); - arguments.append(path); +bool Launcher::MainDialog::setup() +{ + if (!setupGameSettings()) + return false; - if (!startProgram(QString("mwiniimport"), arguments, false)) - return false; + mLauncherSettings.setContentList(mGameSettings); - // Re-read the game settings - if (!setupGameSettings()) - return false; + if (!setupGraphicsSettings()) + return false; - // Add a new profile - if (msgBox.isChecked()) { - mLauncherSettings.setValue(QString("Profiles/currentprofile"), QString("Imported")); - mLauncherSettings.remove(QString("Profiles/Imported/content")); + // Now create the pages as they need the settings + createPages(); - QStringList contents = mGameSettings.values(QString("content")); - foreach (const QString &content, contents) { - mLauncherSettings.setMultiValue(QString("Profiles/Imported/content"), content); - } - } + // Call this so we can exit on Ogre/SDL errors before mainwindow is shown + if (!mGraphicsPage->loadSettings()) + return false; - } + loadSettings(); return true; } -bool Launcher::MainDialog::setup() +bool Launcher::MainDialog::reloadSettings() { if (!setupLauncherSettings()) return false; @@ -307,24 +216,20 @@ bool Launcher::MainDialog::setup() if (!setupGameSettings()) return false; + mLauncherSettings.setContentList(mGameSettings); + if (!setupGraphicsSettings()) return false; - // Check if we need to show the importer - if (mLauncherSettings.value(QString("General/firstrun"), QString("true")) == QLatin1String("true")) - { - if (!showFirstRunDialog()) - return false; - } + if (!mSettingsPage->loadSettings()) + return false; - // Now create the pages as they need the settings - createPages(); + if (!mDataFilesPage->loadSettings()) + return false; - // Call this so we can exit on Ogre/SDL errors before mainwindow is shown if (!mGraphicsPage->loadSettings()) return false; - loadSettings(); return true; } @@ -334,24 +239,8 @@ void Launcher::MainDialog::changePage(QListWidgetItem *current, QListWidgetItem current = previous; int currentIndex = iconWidget->row(current); - int previousIndex = iconWidget->row(previous); - pagesWidget->setCurrentIndex(currentIndex); - - DataFilesPage *previousPage = dynamic_cast(pagesWidget->widget(previousIndex)); - DataFilesPage *currentPage = dynamic_cast(pagesWidget->widget(currentIndex)); - - //special call to update/save data files page list view when it's displayed/hidden. - if (previousPage) - { - if (previousPage->objectName() == "DataFilesPage") - previousPage->saveSettings(); - } - else if (currentPage) - { - if (currentPage->objectName() == "DataFilesPage") - currentPage->loadSettings(); - } + mSettingsPage->resetProgressBar(); } bool Launcher::MainDialog::setupLauncherSettings() @@ -361,8 +250,8 @@ bool Launcher::MainDialog::setupLauncherSettings() QString userPath = QString::fromUtf8(mCfgMgr.getUserConfigPath().string().c_str()); QStringList paths; - paths.append(QString("launcher.cfg")); - paths.append(userPath + QString("launcher.cfg")); + paths.append(QString(Config::LauncherSettings::sLauncherConfigFileName)); + paths.append(userPath + QString(Config::LauncherSettings::sLauncherConfigFileName)); foreach (const QString &path, paths) { qDebug() << "Loading config file:" << qPrintable(path); @@ -373,10 +262,10 @@ bool Launcher::MainDialog::setupLauncherSettings() msgBox.setWindowTitle(tr("Error opening OpenMW configuration file")); msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(QObject::tr("
Could not open %0 for reading

\ - Please make sure you have the right permissions \ - and try again.
").arg(file.fileName())); - msgBox.exec(); + msgBox.setText(tr("
Could not open %0 for reading

\ + Please make sure you have the right permissions \ + and try again.
").arg(file.fileName())); + msgBox.exec(); return false; } QTextStream stream(&file); @@ -390,78 +279,6 @@ bool Launcher::MainDialog::setupLauncherSettings() return true; } -#ifndef WIN32 -bool Launcher::expansions(Launcher::UnshieldThread& cd) -{ - if(cd.BloodmoonDone()) - { - cd.Done(); - return false; - } - - QMessageBox expansionsBox; - expansionsBox.setText(QObject::tr("
Would you like to install expansions now ? (make sure you have the disc)
\ - If you want to install both Bloodmoon and Tribunal, you have to install Tribunal first.
")); - - QAbstractButton* tribunalButton = NULL; - if(!cd.TribunalDone()) - tribunalButton = expansionsBox.addButton(QObject::tr("&Tribunal"), QMessageBox::ActionRole); - - QAbstractButton* bloodmoonButton = expansionsBox.addButton(QObject::tr("&Bloodmoon"), QMessageBox::ActionRole); - QAbstractButton* noneButton = expansionsBox.addButton(QObject::tr("&None"), QMessageBox::ActionRole); - - expansionsBox.exec(); - - if(expansionsBox.clickedButton() == noneButton) - { - cd.Done(); - return false; - } - else if(expansionsBox.clickedButton() == tribunalButton) - { - - TextSlotMsgBox cdbox; - cdbox.setStandardButtons(QMessageBox::Cancel); - - QObject::connect(&cd,SIGNAL(signalGUI(const QString&)), &cdbox, SLOT(setTextSlot(const QString&))); - QObject::connect(&cd,SIGNAL(close()), &cdbox, SLOT(reject())); - - cd.SetTribunalPath( - QFileDialog::getOpenFileName( - NULL, - QObject::tr("Select data1.hdr from Tribunal Installation CD (Tribunal/data1.hdr on GOTY CDs)"), - QDir::currentPath(), - QString(QObject::tr("Installshield hdr file (*.hdr)"))).toUtf8().constData()); - - cd.start(); - cdbox.exec(); - } - else if(expansionsBox.clickedButton() == bloodmoonButton) - { - - TextSlotMsgBox cdbox; - cdbox.setStandardButtons(QMessageBox::Cancel); - - QObject::connect(&cd,SIGNAL(signalGUI(const QString&)), &cdbox, SLOT(setTextSlot(const QString&))); - QObject::connect(&cd,SIGNAL(close()), &cdbox, SLOT(reject())); - - cd.SetBloodmoonPath( - QFileDialog::getOpenFileName( - NULL, - QObject::tr("Select data1.hdr from Bloodmoon Installation CD (Bloodmoon/data1.hdr on GOTY CDs)"), - QDir::currentPath(), - QString(QObject::tr("Installshield hdr file (*.hdr)"))).toUtf8().constData()); - - cd.start(); - cdbox.exec(); - } - - - - return true; -} -#endif // WIN32 - bool Launcher::MainDialog::setupGameSettings() { QString userPath = QString::fromUtf8(mCfgMgr.getUserConfigPath().string().c_str()); @@ -480,7 +297,7 @@ bool Launcher::MainDialog::setupGameSettings() msgBox.setWindowTitle(tr("Error opening OpenMW configuration file")); msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(QObject::tr("
Could not open %0 for reading

\ + msgBox.setText(tr("
Could not open %0 for reading

\ Please make sure you have the right permissions \ and try again.
").arg(file.fileName())); msgBox.exec(); @@ -508,7 +325,7 @@ bool Launcher::MainDialog::setupGameSettings() msgBox.setWindowTitle(tr("Error opening OpenMW configuration file")); msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(QObject::tr("
Could not open %0 for reading

\ + msgBox.setText(tr("
Could not open %0 for reading

\ Please make sure you have the right permissions \ and try again.
").arg(file.fileName())); msgBox.exec(); @@ -540,72 +357,22 @@ bool Launcher::MainDialog::setupGameSettings() msgBox.setWindowTitle(tr("Error detecting Morrowind installation")); msgBox.setIcon(QMessageBox::Warning); msgBox.setStandardButtons(QMessageBox::Cancel); - msgBox.setText(QObject::tr("
Could not find the Data Files location

\ - The directory containing the data files was not found.

\ - Press \"Browse...\" to specify the location manually.
")); - - QAbstractButton *dirSelectButton = - msgBox.addButton(QObject::tr("Browse to &Install..."), QMessageBox::ActionRole); + msgBox.setText(tr("
Could not find the Data Files location

\ + The directory containing the data files was not found.")); - #ifndef WIN32 - QAbstractButton *cdSelectButton = - msgBox.addButton(QObject::tr("Browse to &CD..."), QMessageBox::ActionRole); - #endif + QAbstractButton *wizardButton = + msgBox.addButton(tr("Run &Installation Wizard..."), QMessageBox::ActionRole); + msgBox.exec(); - msgBox.exec(); - - QString selectedFile; - if (msgBox.clickedButton() == dirSelectButton) { - selectedFile = QFileDialog::getOpenFileName( - NULL, - QObject::tr("Select master file"), - QDir::currentPath(), - QString(tr("Morrowind master file (*.esm)"))); - } - #ifndef WIN32 - else if(msgBox.clickedButton() == cdSelectButton) { - UnshieldThread cd; - - { - TextSlotMsgBox cdbox; - cdbox.setStandardButtons(QMessageBox::Cancel); - - QObject::connect(&cd,SIGNAL(signalGUI(const QString&)), &cdbox, SLOT(setTextSlot(const QString&))); - QObject::connect(&cd,SIGNAL(close()), &cdbox, SLOT(reject())); - - cd.SetMorrowindPath( - QFileDialog::getOpenFileName( - NULL, - QObject::tr("Select data1.hdr from Morrowind Installation CD"), - QDir::currentPath(), - QString(tr("Installshield hdr file (*.hdr)"))).toUtf8().constData()); - - cd.SetOutputPath( - QFileDialog::getExistingDirectory( - NULL, - QObject::tr("Select where to extract files to"), - QDir::currentPath(), - QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks).toUtf8().constData()); - - cd.start(); - cdbox.exec(); + if (msgBox.clickedButton() == wizardButton) + { + if (!mWizardInvoker->startProcess(QLatin1String("openmw-wizard"), false)) { + return false; + } else { + return true; } - - while(expansions(cd)); - - selectedFile = QString::fromUtf8(cd.GetMWEsmPath().c_str()); } - #endif // WIN32 - - if (selectedFile.isEmpty()) - return false; // Cancel was clicked; - - QFileInfo info(selectedFile); - - // Add the new dir to the settings file and to the data dir container - mGameSettings.setMultiValue(QString("data"), info.absolutePath()); - mGameSettings.addDataDir(info.absolutePath()); } return true; @@ -626,7 +393,7 @@ bool Launcher::MainDialog::setupGraphicsSettings() msgBox.setWindowTitle(tr("Error reading OpenMW configuration file")); msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(QObject::tr("
Could not find settings-default.cfg

\ + msgBox.setText(tr("
Could not find settings-default.cfg

\ The problem may be due to an incomplete installation of OpenMW.
\ Reinstalling OpenMW may resolve the problem.")); msgBox.exec(); @@ -648,7 +415,7 @@ bool Launcher::MainDialog::setupGraphicsSettings() msgBox.setWindowTitle(tr("Error opening OpenMW configuration file")); msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(QObject::tr("
Could not open %0 for reading

\ + msgBox.setText(tr("
Could not open %0 for reading

\ Please make sure you have the right permissions \ and try again.
").arg(file.fileName())); msgBox.exec(); @@ -699,8 +466,9 @@ bool Launcher::MainDialog::writeSettings() { // Now write all config files saveSettings(); - mGraphicsPage->saveSettings(); mDataFilesPage->saveSettings(); + mGraphicsPage->saveSettings(); + mSettingsPage->saveSettings(); QString userPath = QString::fromUtf8(mCfgMgr.getUserConfigPath().string().c_str()); QDir dir(userPath); @@ -714,8 +482,8 @@ bool Launcher::MainDialog::writeSettings() msgBox.setText(tr("
Could not create %0

\ Please make sure you have the right permissions \ and try again.
").arg(userPath)); - msgBox.exec(); - return false; + msgBox.exec(); + return false; } } @@ -731,8 +499,8 @@ bool Launcher::MainDialog::writeSettings() msgBox.setText(tr("
Could not open or create %0 for writing

\ Please make sure you have the right permissions \ and try again.
").arg(file.fileName())); - msgBox.exec(); - return false; + msgBox.exec(); + return false; } QTextStream stream(&file); @@ -753,8 +521,8 @@ bool Launcher::MainDialog::writeSettings() msgBox.setText(tr("
Could not open or create %0 for writing

\ Please make sure you have the right permissions \ and try again.
").arg(file.fileName())); - msgBox.exec(); - return false; + msgBox.exec(); + return false; } stream.setDevice(&file); @@ -764,7 +532,7 @@ bool Launcher::MainDialog::writeSettings() file.close(); // Launcher settings - file.setFileName(userPath + QString("launcher.cfg")); + file.setFileName(userPath + QString(Config::LauncherSettings::sLauncherConfigFileName)); if (!file.open(QIODevice::ReadWrite | QIODevice::Text | QIODevice::Truncate)) { // File cannot be opened or created @@ -775,8 +543,8 @@ bool Launcher::MainDialog::writeSettings() msgBox.setText(tr("
Could not open or create %0 for writing

\ Please make sure you have the right permissions \ and try again.
").arg(file.fileName())); - msgBox.exec(); - return false; + msgBox.exec(); + return false; } stream.setDevice(&file); @@ -794,122 +562,41 @@ void Launcher::MainDialog::closeEvent(QCloseEvent *event) event->accept(); } -void Launcher::MainDialog::play() +void Launcher::MainDialog::wizardStarted() { - if (!writeSettings()) { - qApp->quit(); - return; - } - - if(!mGameSettings.hasMaster()) { - QMessageBox msgBox; - msgBox.setWindowTitle(tr("No game file selected")); - msgBox.setIcon(QMessageBox::Warning); - msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(tr("
You do not have a game file selected.

\ - OpenMW will not start without a game file selected.
")); - msgBox.exec(); - return; - } - - // Launch the game detached - startProgram(QString("openmw"), true); - qApp->quit(); + hide(); } -bool Launcher::MainDialog::startProgram(const QString &name, const QStringList &arguments, bool detached) +void Launcher::MainDialog::wizardFinished(int exitCode, QProcess::ExitStatus exitStatus) { - QString path = name; -#ifdef Q_OS_WIN - path.append(QString(".exe")); -#elif defined(Q_OS_MAC) - QDir dir(QCoreApplication::applicationDirPath()); - path = dir.absoluteFilePath(name); -#else - path.prepend(QString("./")); -#endif + if (exitCode != 0 || exitStatus == QProcess::CrashExit) + return qApp->quit(); - QFile file(path); - - QProcess process; - QFileInfo info(file); + // HACK: Ensure the pages are created, else segfault + setup(); - if (!file.exists()) { - QMessageBox msgBox; - msgBox.setWindowTitle(tr("Error starting executable")); - msgBox.setIcon(QMessageBox::Warning); - msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(tr("
Could not find %1

\ - The application is not found.
\ - Please make sure OpenMW is installed correctly and try again.
").arg(info.fileName())); - msgBox.exec(); + if (reloadSettings()) + show(); +} - return false; - } +void Launcher::MainDialog::play() +{ + if (!writeSettings()) + return qApp->quit(); - if (!info.isExecutable()) { + if (!mGameSettings.hasMaster()) { QMessageBox msgBox; - msgBox.setWindowTitle(tr("Error starting executable")); + msgBox.setWindowTitle(tr("No game file selected")); msgBox.setIcon(QMessageBox::Warning); msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(tr("
Could not start %1

\ - The application is not executable.
\ - Please make sure you have the right permissions and try again.
").arg(info.fileName())); - msgBox.exec(); - - return false; - } - - // Start the executable - if (detached) { - if (!process.startDetached(path, arguments)) { - QMessageBox msgBox; - msgBox.setWindowTitle(tr("Error starting executable")); - msgBox.setIcon(QMessageBox::Critical); - msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(tr("
Could not start %1

\ - An error occurred while starting %1.

\ - Press \"Show Details...\" for more information.
").arg(info.fileName())); - msgBox.setDetailedText(process.errorString()); - msgBox.exec(); - - return false; - } - } else { - process.start(path, arguments); - if (!process.waitForFinished()) { - QMessageBox msgBox; - msgBox.setWindowTitle(tr("Error starting executable")); - msgBox.setIcon(QMessageBox::Critical); - msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(tr("
Could not start %1

\ - An error occurred while starting %1.

\ - Press \"Show Details...\" for more information.
").arg(info.fileName())); - msgBox.setDetailedText(process.errorString()); - msgBox.exec(); - - return false; - } - - if (process.exitCode() != 0 || process.exitStatus() == QProcess::CrashExit) { - QString error(process.readAllStandardError()); - error.append(tr("\nArguments:\n")); - error.append(arguments.join(" ")); - - QMessageBox msgBox; - msgBox.setWindowTitle(tr("Error running executable")); - msgBox.setIcon(QMessageBox::Critical); - msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setText(tr("
Executable %1 returned an error

\ - An error occurred while running %1.

\ - Press \"Show Details...\" for more information.
").arg(info.fileName())); - msgBox.setDetailedText(error); - msgBox.exec(); - - return false; - } + msgBox.setText(tr("
You do not have a game file selected.

\ + OpenMW will not start without a game file selected.
")); + msgBox.exec(); + return; } - return true; + // Launch the game detached + if (mGameInvoker->startProcess(QLatin1String("openmw"), true)) + return qApp->quit(); } diff --git a/apps/launcher/maindialog.hpp b/apps/launcher/maindialog.hpp index 5b8e4908e..0708f7002 100644 --- a/apps/launcher/maindialog.hpp +++ b/apps/launcher/maindialog.hpp @@ -2,16 +2,26 @@ #define MAINDIALOG_H #include +#include + #ifndef Q_MOC_RUN #include #endif -#include "settings/gamesettings.hpp" + +#include + +#include +#include + #include "settings/graphicssettings.hpp" -#include "settings/launchersettings.hpp" #include "ui_mainwindow.h" class QListWidgetItem; +class QStackedWidget; +class QStringList; +class QStringListModel; +class QString; namespace Launcher { @@ -19,6 +29,7 @@ namespace Launcher class GraphicsPage; class DataFilesPage; class UnshieldThread; + class SettingsPage; #ifndef WIN32 bool expansions(Launcher::UnshieldThread& cd); @@ -30,13 +41,22 @@ namespace Launcher public: explicit MainDialog(QWidget *parent = 0); + ~MainDialog(); + bool setup(); bool showFirstRunDialog(); + bool reloadSettings(); + bool writeSettings(); + public slots: void changePage(QListWidgetItem *current, QListWidgetItem *previous); void play(); + private slots: + void wizardStarted(); + void wizardFinished(int exitCode, QProcess::ExitStatus exitStatus); + private: void createIcons(); void createPages(); @@ -47,7 +67,6 @@ namespace Launcher void loadSettings(); void saveSettings(); - bool writeSettings(); inline bool startProgram(const QString &name, bool detached = false) { return startProgram(name, QStringList(), detached); } bool startProgram(const QString &name, const QStringList &arguments, bool detached = false); @@ -57,12 +76,16 @@ namespace Launcher PlayPage *mPlayPage; GraphicsPage *mGraphicsPage; DataFilesPage *mDataFilesPage; + SettingsPage *mSettingsPage; + + Process::ProcessInvoker *mGameInvoker; + Process::ProcessInvoker *mWizardInvoker; Files::ConfigurationManager mCfgMgr; - GameSettings mGameSettings; + Config::GameSettings mGameSettings; GraphicsSettings mGraphicsSettings; - LauncherSettings mLauncherSettings; + Config::LauncherSettings mLauncherSettings; }; } diff --git a/apps/launcher/settings/graphicssettings.hpp b/apps/launcher/settings/graphicssettings.hpp index 6f7c13547..a52e0aa84 100644 --- a/apps/launcher/settings/graphicssettings.hpp +++ b/apps/launcher/settings/graphicssettings.hpp @@ -1,11 +1,11 @@ #ifndef GRAPHICSSETTINGS_HPP #define GRAPHICSSETTINGS_HPP -#include "settingsbase.hpp" +#include namespace Launcher { - class GraphicsSettings : public SettingsBase > + class GraphicsSettings : public Config::SettingsBase > { public: GraphicsSettings(); diff --git a/apps/launcher/settings/launchersettings.cpp b/apps/launcher/settings/launchersettings.cpp deleted file mode 100644 index 705453555..000000000 --- a/apps/launcher/settings/launchersettings.cpp +++ /dev/null @@ -1,100 +0,0 @@ -#include "launchersettings.hpp" - -#include -#include -#include -#include - -#include - -Launcher::LauncherSettings::LauncherSettings() -{ -} - -Launcher::LauncherSettings::~LauncherSettings() -{ -} - -QStringList Launcher::LauncherSettings::values(const QString &key, Qt::MatchFlags flags) -{ - QMap settings = SettingsBase::getSettings(); - - if (flags == Qt::MatchExactly) - return settings.values(key); - - QStringList result; - - if (flags == Qt::MatchStartsWith) { - QStringList keys = settings.keys(); - - foreach (const QString ¤tKey, keys) { - if (currentKey.startsWith(key)) - result.append(settings.value(currentKey)); - } - } - - return result; -} - -QStringList Launcher::LauncherSettings::subKeys(const QString &key) -{ - QMap settings = SettingsBase::getSettings(); - QStringList keys = settings.uniqueKeys(); - - QRegExp keyRe("(.+)/"); - - QStringList result; - - foreach (const QString ¤tKey, keys) { - if (keyRe.indexIn(currentKey) != -1) { - QString prefixedKey = keyRe.cap(1); - if(prefixedKey.startsWith(key)) { - QString subKey = prefixedKey.remove(key); - if (!subKey.isEmpty()) - result.append(subKey); - } - } - } - - result.removeDuplicates(); - return result; -} - -bool Launcher::LauncherSettings::writeFile(QTextStream &stream) -{ - QString sectionPrefix; - QRegExp sectionRe("([^/]+)/(.+)$"); - QMap settings = SettingsBase::getSettings(); - - QMapIterator i(settings); - i.toBack(); - - while (i.hasPrevious()) { - i.previous(); - - QString prefix; - QString key; - - if (sectionRe.exactMatch(i.key())) { - prefix = sectionRe.cap(1); - key = sectionRe.cap(2); - } - - // Get rid of legacy settings - if (key.contains(QChar('\\'))) - continue; - - if (key == QLatin1String("CurrentProfile")) - continue; - - if (sectionPrefix != prefix) { - sectionPrefix = prefix; - stream << "\n[" << prefix << "]\n"; - } - - stream << key << "=" << i.value() << "\n"; - } - - return true; - -} diff --git a/apps/launcher/settings/launchersettings.hpp b/apps/launcher/settings/launchersettings.hpp deleted file mode 100644 index 8acc389a9..000000000 --- a/apps/launcher/settings/launchersettings.hpp +++ /dev/null @@ -1,21 +0,0 @@ -#ifndef LAUNCHERSETTINGS_HPP -#define LAUNCHERSETTINGS_HPP - -#include "settingsbase.hpp" - -namespace Launcher -{ - class LauncherSettings : public SettingsBase > - { - public: - LauncherSettings(); - ~LauncherSettings(); - - QStringList subKeys(const QString &key); - QStringList values(const QString &key, Qt::MatchFlags flags = Qt::MatchExactly); - - bool writeFile(QTextStream &stream); - - }; -} -#endif // LAUNCHERSETTINGS_HPP diff --git a/apps/launcher/settingspage.cpp b/apps/launcher/settingspage.cpp new file mode 100644 index 000000000..34b4b41a9 --- /dev/null +++ b/apps/launcher/settingspage.cpp @@ -0,0 +1,275 @@ +#include "settingspage.hpp" + +#include +#include +#include +#include + +#include + +#include +#include + +#include "utils/textinputdialog.hpp" +#include "datafilespage.hpp" + +using namespace Process; + +Launcher::SettingsPage::SettingsPage(Files::ConfigurationManager &cfg, + Config::GameSettings &gameSettings, + Config::LauncherSettings &launcherSettings, MainDialog *parent) + : mCfgMgr(cfg) + , mGameSettings(gameSettings) + , mLauncherSettings(launcherSettings) + , QWidget(parent) + , mMain(parent) +{ + setupUi(this); + + QStringList languages; + languages << QLatin1String("English") + << QLatin1String("French") + << QLatin1String("German") + << QLatin1String("Italian") + << QLatin1String("Polish") + << QLatin1String("Russian") + << QLatin1String("Spanish"); + + languageComboBox->addItems(languages); + + mWizardInvoker = new ProcessInvoker(); + mImporterInvoker = new ProcessInvoker(); + resetProgressBar(); + + connect(mWizardInvoker->getProcess(), SIGNAL(started()), + this, SLOT(wizardStarted())); + + connect(mWizardInvoker->getProcess(), SIGNAL(finished(int,QProcess::ExitStatus)), + this, SLOT(wizardFinished(int,QProcess::ExitStatus))); + + connect(mImporterInvoker->getProcess(), SIGNAL(started()), + this, SLOT(importerStarted())); + + connect(mImporterInvoker->getProcess(), SIGNAL(finished(int,QProcess::ExitStatus)), + this, SLOT(importerFinished(int,QProcess::ExitStatus))); + + mProfileDialog = new TextInputDialog(tr("New Content List"), tr("Content List name:"), this); + + connect(mProfileDialog->lineEdit(), SIGNAL(textChanged(QString)), + this, SLOT(updateOkButton(QString))); + + // Detect Morrowind configuration files + QStringList iniPaths; + + foreach (const QString &path, mGameSettings.getDataDirs()) { + QDir dir(path); + dir.setPath(dir.canonicalPath()); // Resolve symlinks + + if (dir.exists(QString("Morrowind.ini"))) + iniPaths.append(dir.absoluteFilePath(QString("Morrowind.ini"))); + else + { + if (!dir.cdUp()) + continue; // Cannot move from Data Files + + if (dir.exists(QString("Morrowind.ini"))) + iniPaths.append(dir.absoluteFilePath(QString("Morrowind.ini"))); + } + } + + if (!iniPaths.isEmpty()) { + settingsComboBox->addItems(iniPaths); + importerButton->setEnabled(true); + } else { + importerButton->setEnabled(false); + } + + loadSettings(); +} + +Launcher::SettingsPage::~SettingsPage() +{ + delete mWizardInvoker; + delete mImporterInvoker; +} + +void Launcher::SettingsPage::on_wizardButton_clicked() +{ + mMain->writeSettings(); + + if (!mWizardInvoker->startProcess(QLatin1String("openmw-wizard"), false)) + return; +} + +void Launcher::SettingsPage::on_importerButton_clicked() +{ + mMain->writeSettings(); + + // Create the file if it doesn't already exist, else the importer will fail + QString path(QString::fromUtf8(mCfgMgr.getUserConfigPath().string().c_str())); + path.append(QLatin1String("openmw.cfg")); + QFile file(path); + + if (!file.exists()) { + if (!file.open(QIODevice::ReadWrite)) { + // File cannot be created + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error writing OpenMW configuration file")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

Could not open or create %1 for writing

\ +

Please make sure you have the right permissions \ + and try again.

").arg(file.fileName())); + msgBox.exec(); + return; + } + + file.close(); + } + + // Construct the arguments to run the importer + QStringList arguments; + + if (addonsCheckBox->isChecked()) + arguments.append(QString("--game-files")); + + arguments.append(QString("--encoding")); + arguments.append(mGameSettings.value(QString("encoding"), QString("win1252"))); + arguments.append(QString("--ini")); + arguments.append(settingsComboBox->currentText()); + arguments.append(QString("--cfg")); + arguments.append(path); + + qDebug() << "arguments " << arguments; + + // start the progress bar as a "bouncing ball" + progressBar->setMaximum(0); + progressBar->setValue(0); + if (!mImporterInvoker->startProcess(QLatin1String("openmw-iniimporter"), arguments, false)) + { + resetProgressBar(); + } +} + +void Launcher::SettingsPage::on_browseButton_clicked() +{ + QString iniFile = QFileDialog::getOpenFileName( + this, + QObject::tr("Select configuration file"), + QDir::currentPath(), + QString(tr("Morrowind configuration file (*.ini)"))); + + + if (iniFile.isEmpty()) + return; + + QFileInfo info(iniFile); + + if (!info.exists() || !info.isReadable()) + return; + + const QString path(QDir::toNativeSeparators(info.absoluteFilePath())); + + if (settingsComboBox->findText(path) == -1) { + settingsComboBox->addItem(path); + settingsComboBox->setCurrentIndex(settingsComboBox->findText(path)); + importerButton->setEnabled(true); + } +} + +void Launcher::SettingsPage::wizardStarted() +{ + mMain->hide(); // Hide the launcher + + wizardButton->setEnabled(false); +} + +void Launcher::SettingsPage::wizardFinished(int exitCode, QProcess::ExitStatus exitStatus) +{ + if (exitCode != 0 || exitStatus == QProcess::CrashExit) + return qApp->quit(); + + mMain->reloadSettings(); + wizardButton->setEnabled(true); + + mMain->show(); // Show the launcher again +} + +void Launcher::SettingsPage::importerStarted() +{ + importerButton->setEnabled(false); +} + +void Launcher::SettingsPage::importerFinished(int exitCode, QProcess::ExitStatus exitStatus) +{ + if (exitCode != 0 || exitStatus == QProcess::CrashExit) + { + resetProgressBar(); + + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Importer finished")); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setIcon(QMessageBox::Warning); + msgBox.setText(tr("Failed to import settings from INI file.")); + msgBox.exec(); + } + else + { + // indicate progress finished + progressBar->setMaximum(1); + progressBar->setValue(1); + + // Importer may have changed settings, so refresh + mMain->reloadSettings(); + } + + importerButton->setEnabled(true); +} + +void Launcher::SettingsPage::resetProgressBar() +{ + // set progress bar to 0 % + progressBar->reset(); +} + +void Launcher::SettingsPage::updateOkButton(const QString &text) +{ + // We do this here because we need to access the profiles + if (text.isEmpty()) { + mProfileDialog->setOkButtonEnabled(false); + return; + } + + const QStringList profiles(mLauncherSettings.getContentLists()); + + (profiles.contains(text)) + ? mProfileDialog->setOkButtonEnabled(false) + : mProfileDialog->setOkButtonEnabled(true); +} + +void Launcher::SettingsPage::saveSettings() +{ + QString language(languageComboBox->currentText()); + + mLauncherSettings.setValue(QLatin1String("Settings/language"), language); + + if (language == QLatin1String("Polish")) { + mGameSettings.setValue(QLatin1String("encoding"), QLatin1String("win1250")); + } else if (language == QLatin1String("Russian")) { + mGameSettings.setValue(QLatin1String("encoding"), QLatin1String("win1251")); + } else { + mGameSettings.setValue(QLatin1String("encoding"), QLatin1String("win1252")); + } +} + +bool Launcher::SettingsPage::loadSettings() +{ + QString language(mLauncherSettings.value(QLatin1String("Settings/language"))); + + int index = languageComboBox->findText(language); + + if (index != -1) + languageComboBox->setCurrentIndex(index); + + return true; +} diff --git a/apps/launcher/settingspage.hpp b/apps/launcher/settingspage.hpp new file mode 100644 index 000000000..ccc2061dd --- /dev/null +++ b/apps/launcher/settingspage.hpp @@ -0,0 +1,66 @@ +#ifndef SETTINGSPAGE_HPP +#define SETTINGSPAGE_HPP + +#include +#include + +#include + +#include "ui_settingspage.h" + +#include "maindialog.hpp" + +namespace Files { struct ConfigurationManager; } +namespace Config { class GameSettings; + class LauncherSettings; } + +namespace Launcher +{ + class TextInputDialog; + + class SettingsPage : public QWidget, private Ui::SettingsPage + { + Q_OBJECT + + public: + SettingsPage(Files::ConfigurationManager &cfg, Config::GameSettings &gameSettings, + Config::LauncherSettings &launcherSettings, MainDialog *parent = 0); + ~SettingsPage(); + + void saveSettings(); + bool loadSettings(); + + /// set progress bar on page to 0% + void resetProgressBar(); + + private slots: + + void on_wizardButton_clicked(); + void on_importerButton_clicked(); + void on_browseButton_clicked(); + + void wizardStarted(); + void wizardFinished(int exitCode, QProcess::ExitStatus exitStatus); + + void importerStarted(); + void importerFinished(int exitCode, QProcess::ExitStatus exitStatus); + + void updateOkButton(const QString &text); + + private: + + Process::ProcessInvoker *mWizardInvoker; + Process::ProcessInvoker *mImporterInvoker; + + Files::ConfigurationManager &mCfgMgr; + + Config::GameSettings &mGameSettings; + Config::LauncherSettings &mLauncherSettings; + + MainDialog *mMain; + TextInputDialog *mProfileDialog; + + }; +} + +#endif // SETTINGSPAGE_HPP diff --git a/apps/launcher/unshieldthread.cpp b/apps/launcher/unshieldthread.cpp deleted file mode 100644 index 3d0f2e5da..000000000 --- a/apps/launcher/unshieldthread.cpp +++ /dev/null @@ -1,521 +0,0 @@ -#include "unshieldthread.hpp" - -#include -#include - -namespace bfs = boost::filesystem; - -namespace -{ - static bool make_sure_directory_exists(bfs::path directory) - { - - if(!bfs::exists(directory)) - { - bfs::create_directories(directory); - } - - return bfs::exists(directory); - } - - void fill_path(bfs::path& path, const std::string& name) - { - size_t start = 0; - - size_t i; - for(i = 0; i < name.length(); i++) - { - switch(name[i]) - { - case '\\': - path /= name.substr(start, i-start); - start = i+1; - break; - } - } - - path /= name.substr(start, i-start); - } - - std::string get_setting(const std::string& category, const std::string& setting, const std::string& inx) - { - size_t start = inx.find(category); - start = inx.find(setting, start) + setting.length() + 3; - - size_t end = inx.find("!", start); - - return inx.substr(start, end-start); - } - - std::string read_to_string(const bfs::path& path) - { - bfs::ifstream strstream(path, std::ios::in | std::ios::binary); - std::string str; - - strstream.seekg(0, std::ios::end); - str.resize(strstream.tellg()); - strstream.seekg(0, std::ios::beg); - strstream.read(&str[0], str.size()); - strstream.close(); - - return str; - } - - void add_setting(const std::string& category, const std::string& setting, const std::string& val, std::string& ini) - { - size_t loc; - loc = ini.find("[" + category + "]"); - - // If category is not found, create it - if(loc == std::string::npos) - { - loc = ini.size() + 2; - ini += ("\r\n[" + category + "]\r\n"); - } - - loc += category.length() +2 +2; - ini.insert(loc, setting + "=" + val + "\r\n"); - } - - #define FIX(setting) add_setting(category, setting, get_setting(category, setting, inx), ini) - - void bloodmoon_fix_ini(std::string& ini, const bfs::path inxPath) - { - std::string inx = read_to_string(inxPath); - - // Remove this one setting (the only one actually changed by bloodmoon, as opposed to just adding new ones) - size_t start = ini.find("[Weather Blight]"); - start = ini.find("Ambient Loop Sound ID", start); - size_t end = ini.find("\r\n", start) +2; - ini.erase(start, end-start); - - std::string category; - - category = "General"; - { - FIX("Werewolf FOV"); - } - category = "Moons"; - { - FIX("Script Color"); - } - category = "Weather"; - { - FIX("Snow Ripples"); - FIX("Snow Ripple Radius"); - FIX("Snow Ripples Per Flake"); - FIX("Snow Ripple Scale"); - FIX("Snow Ripple Speed"); - FIX("Snow Gravity Scale"); - FIX("Snow High Kill"); - FIX("Snow Low Kill"); - } - category = "Weather Blight"; - { - FIX("Ambient Loop Sound ID"); - } - category = "Weather Snow"; - { - FIX("Sky Sunrise Color"); - FIX("Sky Day Color"); - FIX("Sky Sunset Color"); - FIX("Sky Night Color"); - FIX("Fog Sunrise Color"); - FIX("Fog Day Color"); - FIX("Fog Sunset Color"); - FIX("Fog Night Color"); - FIX("Ambient Sunrise Color"); - FIX("Ambient Day Color"); - FIX("Ambient Sunset Color"); - FIX("Ambient Night Color"); - FIX("Sun Sunrise Color"); - FIX("Sun Day Color"); - FIX("Sun Sunset Color"); - FIX("Sun Night Color"); - FIX("Sun Disc Sunset Color"); - FIX("Transition Delta"); - FIX("Land Fog Day Depth"); - FIX("Land Fog Night Depth"); - FIX("Clouds Maximum Percent"); - FIX("Wind Speed"); - FIX("Cloud Speed"); - FIX("Glare View"); - FIX("Cloud Texture"); - FIX("Ambient Loop Sound ID"); - FIX("Snow Threshold"); - FIX("Snow Diameter"); - FIX("Snow Height Min"); - FIX("Snow Height Max"); - FIX("Snow Entrance Speed"); - FIX("Max Snowflakes"); - } - category = "Weather Blizzard"; - { - FIX("Sky Sunrise Color"); - FIX("Sky Day Color"); - FIX("Sky Sunset Color"); - FIX("Sky Night Color"); - FIX("Fog Sunrise Color"); - FIX("Fog Day Color"); - FIX("Fog Sunset Color"); - FIX("Fog Night Color"); - FIX("Ambient Sunrise Color"); - FIX("Ambient Day Color"); - FIX("Ambient Sunset Color"); - FIX("Ambient Night Color"); - FIX("Sun Sunrise Color"); - FIX("Sun Day Color"); - FIX("Sun Sunset Color"); - FIX("Sun Night Color"); - FIX("Sun Disc Sunset Color"); - FIX("Transition Delta"); - FIX("Land Fog Day Depth"); - FIX("Land Fog Night Depth"); - FIX("Clouds Maximum Percent"); - FIX("Wind Speed"); - FIX("Cloud Speed"); - FIX("Glare View"); - FIX("Cloud Texture"); - FIX("Ambient Loop Sound ID"); - FIX("Storm Threshold"); - } - } - - - void fix_ini(const bfs::path& output_dir, bfs::path cdPath, bool tribunal, bool bloodmoon) - { - bfs::path ini_path = output_dir; - ini_path /= "Morrowind.ini"; - - std::string ini = read_to_string(ini_path.string()); - - if(tribunal) - { - add_setting("Game Files", "GameFile1", "Tribunal.esm", ini); - add_setting("Archives", "Archive 0", "Tribunal.bsa", ini); - } - if(bloodmoon) - { - bloodmoon_fix_ini(ini, cdPath / "setup.inx"); - add_setting("Game Files", "GameFile2", "Bloodmoon.esm", ini); - add_setting("Archives", "Archive 1", "Bloodmoon.bsa", ini); - } - - bfs::ofstream inistream((ini_path)); - inistream << ini; - inistream.close(); - } - - void installToPath(const bfs::path& from, const bfs::path& to, bool copy = false) - { - make_sure_directory_exists(to); - - for ( bfs::directory_iterator end, dir(from); dir != end; ++dir ) - { - if(bfs::is_directory(dir->path())) - installToPath(dir->path(), to / dir->path().filename(), copy); - else - { - if(copy) - { - bfs::path dest = to / dir->path().filename(); - if(bfs::exists(dest)) - bfs::remove_all(dest); - bfs::copy_file(dir->path(), dest); - } - else - bfs::rename(dir->path(), to / dir->path().filename()); - } - } - } - - bfs::path findFile(const bfs::path& in, std::string filename, bool recursive = true) - { - if(recursive) - { - for ( bfs::recursive_directory_iterator end, dir(in); dir != end; ++dir ) - { - if(Misc::StringUtils::ciEqual(dir->path().filename().string(), filename)) - return dir->path(); - } - } - else - { - for ( bfs::directory_iterator end, dir(in); dir != end; ++dir ) - { - if(Misc::StringUtils::ciEqual(dir->path().filename().string(), filename)) - return dir->path(); - } - } - - return ""; - } - - bool contains(const bfs::path& in, std::string filename) - { - for(bfs::directory_iterator end, dir(in); dir != end; ++dir) - { - if(Misc::StringUtils::ciEqual(dir->path().filename().string(), filename)) - return true; - } - - return false; - } - - time_t getTime(const char* time) - { - struct tm tms; - memset(&tms, 0, sizeof(struct tm)); - strptime(time, "%d %B %Y", &tms); - return mktime(&tms); - } - - // Some cds have cab files which have the Data Files subfolders outside the Data Files folder - void install_dfiles_outside(const bfs::path& from, const bfs::path& dFiles) - { - bfs::path fonts = findFile(from, "fonts", false); - if(fonts.string() != "") - installToPath(fonts, dFiles / "Fonts"); - - bfs::path music = findFile(from, "music", false); - if(music.string() != "") - installToPath(music, dFiles / "Music"); - - bfs::path sound = findFile(from, "sound", false); - if(sound.string() != "") - installToPath(sound, dFiles / "Sound"); - - bfs::path splash = findFile(from, "splash", false); - if(splash.string() != "") - installToPath(splash, dFiles / "Splash"); - } - -} - -bool Launcher::UnshieldThread::SetMorrowindPath(const std::string& path) -{ - mMorrowindPath = path; - return true; -} - -bool Launcher::UnshieldThread::SetTribunalPath(const std::string& path) -{ - mTribunalPath = path; - return true; -} - -bool Launcher::UnshieldThread::SetBloodmoonPath(const std::string& path) -{ - mBloodmoonPath = path; - return true; -} - -void Launcher::UnshieldThread::SetOutputPath(const std::string& path) -{ - mOutputPath = path; -} - -bool Launcher::UnshieldThread::extract_file(Unshield* unshield, bfs::path output_dir, const char* prefix, int index) -{ - bool success; - bfs::path dirname; - bfs::path filename; - int directory = unshield_file_directory(unshield, index); - - dirname = output_dir; - - if (prefix && prefix[0]) - dirname /= prefix; - - if (directory >= 0) - { - const char* tmp = unshield_directory_name(unshield, directory); - if (tmp && tmp[0]) - fill_path(dirname, tmp); - } - - make_sure_directory_exists(dirname); - - filename = dirname; - filename /= unshield_file_name(unshield, index); - - emit signalGUI(QString("Extracting: ") + QString(filename.c_str())); - - success = unshield_file_save(unshield, index, filename.c_str()); - - if (!success) - bfs::remove(filename); - - return success; -} - -void Launcher::UnshieldThread::extract_cab(const bfs::path& cab, const bfs::path& output_dir, bool extract_ini) -{ - Unshield * unshield; - unshield = unshield_open(cab.c_str()); - - int i; - for (i = 0; i < unshield_file_group_count(unshield); i++) - { - UnshieldFileGroup* file_group = unshield_file_group_get(unshield, i); - - for (size_t j = file_group->first_file; j <= file_group->last_file; j++) - { - if (unshield_file_is_valid(unshield, j)) - extract_file(unshield, output_dir, file_group->name, j); - } - } - unshield_close(unshield); -} - - -bool Launcher::UnshieldThread::extract() -{ - bfs::path outputDataFilesDir = mOutputPath; - outputDataFilesDir /= "Data Files"; - bfs::path extractPath = mOutputPath; - extractPath /= "extract-temp"; - - if(!mMorrowindDone && mMorrowindPath.string().length() > 0) - { - mMorrowindDone = true; - - bfs::path mwExtractPath = extractPath / "morrowind"; - extract_cab(mMorrowindPath, mwExtractPath, true); - - bfs::path dFilesDir = findFile(mwExtractPath, "morrowind.esm").parent_path(); - - installToPath(dFilesDir, outputDataFilesDir); - - install_dfiles_outside(mwExtractPath, outputDataFilesDir); - - // Videos are often kept uncompressed on the cd - bfs::path videosPath = findFile(mMorrowindPath.parent_path(), "video", false); - if(videosPath.string() != "") - { - emit signalGUI(QString("Installing Videos...")); - installToPath(videosPath, outputDataFilesDir / "Video", true); - } - - bfs::path cdDFiles = findFile(mMorrowindPath.parent_path(), "data files", false); - if(cdDFiles.string() != "") - { - emit signalGUI(QString("Installing Uncompressed Data files from CD...")); - installToPath(cdDFiles, outputDataFilesDir, true); - } - - - bfs::rename(findFile(mwExtractPath, "morrowind.ini"), outputDataFilesDir / "Morrowind.ini"); - - mTribunalDone = contains(outputDataFilesDir, "tribunal.esm"); - mBloodmoonDone = contains(outputDataFilesDir, "bloodmoon.esm"); - - } - - else if(!mTribunalDone && mTribunalPath.string().length() > 0) - { - mTribunalDone = true; - - bfs::path tbExtractPath = extractPath / "tribunal"; - extract_cab(mTribunalPath, tbExtractPath, true); - - bfs::path dFilesDir = findFile(tbExtractPath, "tribunal.esm").parent_path(); - - installToPath(dFilesDir, outputDataFilesDir); - - install_dfiles_outside(tbExtractPath, outputDataFilesDir); - - // Mt GOTY CD has Sounds in a seperate folder from the rest of the data files - bfs::path soundsPath = findFile(tbExtractPath, "sounds", false); - if(soundsPath.string() != "") - installToPath(soundsPath, outputDataFilesDir / "Sounds"); - - bfs::path cdDFiles = findFile(mTribunalPath.parent_path(), "data files", false); - if(cdDFiles.string() != "") - { - emit signalGUI(QString("Installing Uncompressed Data files from CD...")); - installToPath(cdDFiles, outputDataFilesDir, true); - } - - mBloodmoonDone = contains(outputDataFilesDir, "bloodmoon.esm"); - - fix_ini(outputDataFilesDir, bfs::path(mTribunalPath).parent_path(), mTribunalDone, mBloodmoonDone); - } - - else if(!mBloodmoonDone && mBloodmoonPath.string().length() > 0) - { - mBloodmoonDone = true; - - bfs::path bmExtractPath = extractPath / "bloodmoon"; - extract_cab(mBloodmoonPath, bmExtractPath, true); - - bfs::path dFilesDir = findFile(bmExtractPath, "bloodmoon.esm").parent_path(); - - installToPath(dFilesDir, outputDataFilesDir); - - install_dfiles_outside(bmExtractPath, outputDataFilesDir); - - // My GOTY CD contains a folder within cab files called Tribunal patch, - // which contains Tribunal.esm - bfs::path tbPatchPath = findFile(bmExtractPath, "tribunal.esm"); - if(tbPatchPath.string() != "") - bfs::rename(tbPatchPath, outputDataFilesDir / "Tribunal.esm"); - - bfs::path cdDFiles = findFile(mBloodmoonPath.parent_path(), "data files", false); - if(cdDFiles.string() != "") - { - emit signalGUI(QString("Installing Uncompressed Data files from CD...")); - installToPath(cdDFiles, outputDataFilesDir, true); - } - - fix_ini(outputDataFilesDir, bfs::path(mBloodmoonPath).parent_path(), false, mBloodmoonDone); - } - - - return true; -} - -void Launcher::UnshieldThread::Done() -{ - // Get rid of unnecessary files - bfs::remove_all(mOutputPath / "extract-temp"); - - // Set modified time to release dates, to preserve load order - if(mMorrowindDone) - bfs::last_write_time(findFile(mOutputPath, "morrowind.esm"), getTime("1 May 2002")); - - if(mTribunalDone) - bfs::last_write_time(findFile(mOutputPath, "tribunal.esm"), getTime("6 November 2002")); - - if(mBloodmoonDone) - bfs::last_write_time(findFile(mOutputPath, "bloodmoon.esm"), getTime("3 June 2003")); -} - -std::string Launcher::UnshieldThread::GetMWEsmPath() -{ - return findFile(mOutputPath / "Data Files", "morrowind.esm").string(); -} - -bool Launcher::UnshieldThread::TribunalDone() -{ - return mTribunalDone; -} - -bool Launcher::UnshieldThread::BloodmoonDone() -{ - return mBloodmoonDone; -} - -void Launcher::UnshieldThread::run() -{ - extract(); - emit close(); -} - -Launcher::UnshieldThread::UnshieldThread() -{ - unshield_set_log_level(0); - mMorrowindDone = false; - mTribunalDone = false; - mBloodmoonDone = false; -} diff --git a/apps/launcher/unshieldthread.hpp b/apps/launcher/unshieldthread.hpp deleted file mode 100644 index de6a32b44..000000000 --- a/apps/launcher/unshieldthread.hpp +++ /dev/null @@ -1,58 +0,0 @@ -#ifndef UNSHIELD_THREAD_H -#define UNSHIELD_THREAD_H - -#include - -#include - -#include - -namespace Launcher -{ - class UnshieldThread : public QThread - { - Q_OBJECT - - public: - bool SetMorrowindPath(const std::string& path); - bool SetTribunalPath(const std::string& path); - bool SetBloodmoonPath(const std::string& path); - - void SetOutputPath(const std::string& path); - - bool extract(); - - bool TribunalDone(); - bool BloodmoonDone(); - - void Done(); - - std::string GetMWEsmPath(); - - UnshieldThread(); - - private: - - void extract_cab(const boost::filesystem::path& cab, const boost::filesystem::path& output_dir, bool extract_ini = false); - bool extract_file(Unshield* unshield, boost::filesystem::path output_dir, const char* prefix, int index); - - boost::filesystem::path mMorrowindPath; - boost::filesystem::path mTribunalPath; - boost::filesystem::path mBloodmoonPath; - - bool mMorrowindDone; - bool mTribunalDone; - bool mBloodmoonDone; - - boost::filesystem::path mOutputPath; - - - protected: - virtual void run(); - - signals: - void signalGUI(QString); - void close(); - }; -} -#endif diff --git a/apps/launcher/utils/checkablemessagebox.cpp b/apps/launcher/utils/checkablemessagebox.cpp deleted file mode 100644 index 2f775af57..000000000 --- a/apps/launcher/utils/checkablemessagebox.cpp +++ /dev/null @@ -1,258 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). -** Contact: http://www.qt-project.org/legal -** -** This file is part of Qt Creator. -** -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and Digia. For licensing terms and -** conditions see http://qt.digia.com/licensing. For further information -** use the contact form at http://qt.digia.com/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 2.1 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 2.1 requirements -** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. -** -** In addition, as a special exception, Digia gives you certain additional -** rights. These rights are described in the Digia Qt LGPL Exception -** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. -** -****************************************************************************/ - -#include "checkablemessagebox.hpp" - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - - -/*! - \class Utils::CheckableMessageBox - - \brief A messagebox suitable for questions with a - "Do not ask me again" checkbox. - - Emulates the QMessageBox API with - static conveniences. The message label can open external URLs. -*/ -Launcher::CheckableMessageBoxPrivate::CheckableMessageBoxPrivate(QDialog *q) - : clickedButton(0) -{ - QSizePolicy sizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred); - - pixmapLabel = new QLabel(q); - sizePolicy.setHorizontalStretch(0); - sizePolicy.setVerticalStretch(0); - sizePolicy.setHeightForWidth(pixmapLabel->sizePolicy().hasHeightForWidth()); - pixmapLabel->setSizePolicy(sizePolicy); - pixmapLabel->setVisible(false); - - QSpacerItem *pixmapSpacer = - new QSpacerItem(0, 5, QSizePolicy::Minimum, QSizePolicy::MinimumExpanding); - - messageLabel = new QLabel(q); - messageLabel->setMinimumSize(QSize(300, 0)); - messageLabel->setWordWrap(true); - messageLabel->setOpenExternalLinks(true); - messageLabel->setTextInteractionFlags(Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse); - - QSpacerItem *checkBoxRightSpacer = - new QSpacerItem(1, 1, QSizePolicy::Expanding, QSizePolicy::Minimum); - QSpacerItem *buttonSpacer = - new QSpacerItem(0, 1, QSizePolicy::Minimum, QSizePolicy::Minimum); - - checkBox = new QCheckBox(q); - checkBox->setText(Launcher::CheckableMessageBox::tr("Do not ask again")); - - buttonBox = new QDialogButtonBox(q); - buttonBox->setOrientation(Qt::Horizontal); - buttonBox->setStandardButtons(QDialogButtonBox::Cancel|QDialogButtonBox::Ok); - - QVBoxLayout *verticalLayout = new QVBoxLayout(); - verticalLayout->addWidget(pixmapLabel); - verticalLayout->addItem(pixmapSpacer); - - QHBoxLayout *horizontalLayout_2 = new QHBoxLayout(); - horizontalLayout_2->addLayout(verticalLayout); - horizontalLayout_2->addWidget(messageLabel); - - QHBoxLayout *horizontalLayout = new QHBoxLayout(); - horizontalLayout->addWidget(checkBox); - horizontalLayout->addItem(checkBoxRightSpacer); - - QVBoxLayout *verticalLayout_2 = new QVBoxLayout(q); - verticalLayout_2->addLayout(horizontalLayout_2); - verticalLayout_2->addLayout(horizontalLayout); - verticalLayout_2->addItem(buttonSpacer); - verticalLayout_2->addWidget(buttonBox); -} - -Launcher::CheckableMessageBox::CheckableMessageBox(QWidget *parent) : - QDialog(parent), - d(new Launcher::CheckableMessageBoxPrivate(this)) -{ - setModal(true); - setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); - connect(d->buttonBox, SIGNAL(accepted()), SLOT(accept())); - connect(d->buttonBox, SIGNAL(rejected()), SLOT(reject())); - connect(d->buttonBox, SIGNAL(clicked(QAbstractButton*)), - SLOT(slotClicked(QAbstractButton*))); -} - -Launcher::CheckableMessageBox::~CheckableMessageBox() -{ - delete d; -} - -void Launcher::CheckableMessageBox::slotClicked(QAbstractButton *b) -{ - d->clickedButton = b; -} - -QAbstractButton *Launcher::CheckableMessageBox::clickedButton() const -{ - return d->clickedButton; -} - -QDialogButtonBox::StandardButton Launcher::CheckableMessageBox::clickedStandardButton() const -{ - if (d->clickedButton) - return d->buttonBox->standardButton(d->clickedButton); - return QDialogButtonBox::NoButton; -} - -QString Launcher::CheckableMessageBox::text() const -{ - return d->messageLabel->text(); -} - -void Launcher::CheckableMessageBox::setText(const QString &t) -{ - d->messageLabel->setText(t); -} - -QPixmap Launcher::CheckableMessageBox::iconPixmap() const -{ - if (const QPixmap *p = d->pixmapLabel->pixmap()) - return QPixmap(*p); - return QPixmap(); -} - -void Launcher::CheckableMessageBox::setIconPixmap(const QPixmap &p) -{ - d->pixmapLabel->setPixmap(p); - d->pixmapLabel->setVisible(!p.isNull()); -} - -bool Launcher::CheckableMessageBox::isChecked() const -{ - return d->checkBox->isChecked(); -} - -void Launcher::CheckableMessageBox::setChecked(bool s) -{ - d->checkBox->setChecked(s); -} - -QString Launcher::CheckableMessageBox::checkBoxText() const -{ - return d->checkBox->text(); -} - -void Launcher::CheckableMessageBox::setCheckBoxText(const QString &t) -{ - d->checkBox->setText(t); -} - -bool Launcher::CheckableMessageBox::isCheckBoxVisible() const -{ - return d->checkBox->isVisible(); -} - -void Launcher::CheckableMessageBox::setCheckBoxVisible(bool v) -{ - d->checkBox->setVisible(v); -} - -QDialogButtonBox::StandardButtons Launcher::CheckableMessageBox::standardButtons() const -{ - return d->buttonBox->standardButtons(); -} - -void Launcher::CheckableMessageBox::setStandardButtons(QDialogButtonBox::StandardButtons s) -{ - d->buttonBox->setStandardButtons(s); -} - -QPushButton *Launcher::CheckableMessageBox::button(QDialogButtonBox::StandardButton b) const -{ - return d->buttonBox->button(b); -} - -QPushButton *Launcher::CheckableMessageBox::addButton(const QString &text, QDialogButtonBox::ButtonRole role) -{ - return d->buttonBox->addButton(text, role); -} - -QDialogButtonBox::StandardButton Launcher::CheckableMessageBox::defaultButton() const -{ - foreach (QAbstractButton *b, d->buttonBox->buttons()) - if (QPushButton *pb = qobject_cast(b)) - if (pb->isDefault()) - return d->buttonBox->standardButton(pb); - return QDialogButtonBox::NoButton; -} - -void Launcher::CheckableMessageBox::setDefaultButton(QDialogButtonBox::StandardButton s) -{ - if (QPushButton *b = d->buttonBox->button(s)) { - b->setDefault(true); - b->setFocus(); - } -} - -QDialogButtonBox::StandardButton -Launcher::CheckableMessageBox::question(QWidget *parent, - const QString &title, - const QString &question, - const QString &checkBoxText, - bool *checkBoxSetting, - QDialogButtonBox::StandardButtons buttons, - QDialogButtonBox::StandardButton defaultButton) -{ - CheckableMessageBox mb(parent); - mb.setWindowTitle(title); - mb.setIconPixmap(QMessageBox::standardIcon(QMessageBox::Question)); - mb.setText(question); - mb.setCheckBoxText(checkBoxText); - mb.setChecked(*checkBoxSetting); - mb.setStandardButtons(buttons); - mb.setDefaultButton(defaultButton); - mb.exec(); - *checkBoxSetting = mb.isChecked(); - return mb.clickedStandardButton(); -} - -QMessageBox::StandardButton Launcher::CheckableMessageBox::dialogButtonBoxToMessageBoxButton(QDialogButtonBox::StandardButton db) -{ - return static_cast(int(db)); -} diff --git a/apps/launcher/utils/checkablemessagebox.hpp b/apps/launcher/utils/checkablemessagebox.hpp deleted file mode 100644 index 09a501b9c..000000000 --- a/apps/launcher/utils/checkablemessagebox.hpp +++ /dev/null @@ -1,116 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). -** Contact: http://www.qt-project.org/legal -** -** This file is part of Qt Creator. -** -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and Digia. For licensing terms and -** conditions see http://qt.digia.com/licensing. For further information -** use the contact form at http://qt.digia.com/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 2.1 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 2.1 requirements -** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. -** -** In addition, as a special exception, Digia gives you certain additional -** rights. These rights are described in the Digia Qt LGPL Exception -** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. -** -****************************************************************************/ - -#ifndef CHECKABLEMESSAGEBOX_HPP -#define CHECKABLEMESSAGEBOX_HPP - -#include -#include -#include - -class QCheckBox; - -namespace Launcher -{ - class CheckableMessageBoxPrivate - { - public: - - QLabel *pixmapLabel; - QLabel *messageLabel; - QCheckBox *checkBox; - QDialogButtonBox *buttonBox; - QAbstractButton *clickedButton; - - public: - CheckableMessageBoxPrivate(QDialog *q); - }; - - class CheckableMessageBox : public QDialog - { - Q_OBJECT - Q_PROPERTY(QString text READ text WRITE setText) - Q_PROPERTY(QPixmap iconPixmap READ iconPixmap WRITE setIconPixmap) - Q_PROPERTY(bool isChecked READ isChecked WRITE setChecked) - Q_PROPERTY(QString checkBoxText READ checkBoxText WRITE setCheckBoxText) - Q_PROPERTY(QDialogButtonBox::StandardButtons buttons READ standardButtons WRITE setStandardButtons) - Q_PROPERTY(QDialogButtonBox::StandardButton defaultButton READ defaultButton WRITE setDefaultButton) - - public: - explicit CheckableMessageBox(QWidget *parent); - virtual ~CheckableMessageBox(); - - static QDialogButtonBox::StandardButton - question(QWidget *parent, - const QString &title, - const QString &question, - const QString &checkBoxText, - bool *checkBoxSetting, - QDialogButtonBox::StandardButtons buttons = QDialogButtonBox::Yes|QDialogButtonBox::No, - QDialogButtonBox::StandardButton defaultButton = QDialogButtonBox::No); - - QString text() const; - void setText(const QString &); - - bool isChecked() const; - void setChecked(bool s); - - QString checkBoxText() const; - void setCheckBoxText(const QString &); - - bool isCheckBoxVisible() const; - void setCheckBoxVisible(bool); - - QDialogButtonBox::StandardButtons standardButtons() const; - void setStandardButtons(QDialogButtonBox::StandardButtons s); - QPushButton *button(QDialogButtonBox::StandardButton b) const; - QPushButton *addButton(const QString &text, QDialogButtonBox::ButtonRole role); - - QDialogButtonBox::StandardButton defaultButton() const; - void setDefaultButton(QDialogButtonBox::StandardButton s); - - // See static QMessageBox::standardPixmap() - QPixmap iconPixmap() const; - void setIconPixmap (const QPixmap &p); - - // Query the result - QAbstractButton *clickedButton() const; - QDialogButtonBox::StandardButton clickedStandardButton() const; - - // Conversion convenience - static QMessageBox::StandardButton dialogButtonBoxToMessageBoxButton(QDialogButtonBox::StandardButton); - - private slots: - void slotClicked(QAbstractButton *b); - - private: - CheckableMessageBoxPrivate *d; - }; -} -#endif // CHECKABLEMESSAGEBOX_HPP diff --git a/apps/launcher/utils/profilescombobox.cpp b/apps/launcher/utils/profilescombobox.cpp index c14330724..7df89098e 100644 --- a/apps/launcher/utils/profilescombobox.cpp +++ b/apps/launcher/utils/profilescombobox.cpp @@ -47,13 +47,13 @@ void ProfilesComboBox::setEditEnabled(bool editable) void ProfilesComboBox::slotTextChanged(const QString &text) { - QPalette *palette = new QPalette(); - palette->setColor(QPalette::Text,Qt::red); + QPalette palette; + palette.setColor(QPalette::Text,Qt::red); int index = findText(text); if (text.isEmpty() || (index != -1 && index != currentIndex())) { - lineEdit()->setPalette(*palette); + lineEdit()->setPalette(palette); } else { lineEdit()->setPalette(QApplication::palette()); } diff --git a/apps/launcher/utils/textinputdialog.cpp b/apps/launcher/utils/textinputdialog.cpp index 76cbe32d0..385d086fd 100644 --- a/apps/launcher/utils/textinputdialog.cpp +++ b/apps/launcher/utils/textinputdialog.cpp @@ -16,15 +16,15 @@ Launcher::TextInputDialog::TextInputDialog(const QString& title, const QString & mButtonBox->addButton(QDialogButtonBox::Cancel); mButtonBox->button(QDialogButtonBox::Ok)->setEnabled (false); + QLabel *label = new QLabel(this); + label->setText(text); + // Line edit QValidator *validator = new QRegExpValidator(QRegExp("^[a-zA-Z0-9_]*$"), this); // Alpha-numeric + underscore - mLineEdit = new DialogLineEdit(this); + mLineEdit = new LineEdit(this); mLineEdit->setValidator(validator); mLineEdit->setCompleter(0); - QLabel *label = new QLabel(this); - label->setText(text); - QVBoxLayout *dialogLayout = new QVBoxLayout(this); dialogLayout->addWidget(label); dialogLayout->addWidget(mLineEdit); @@ -41,8 +41,10 @@ Launcher::TextInputDialog::TextInputDialog(const QString& title, const QString & connect(mButtonBox, SIGNAL(accepted()), this, SLOT(accept())); connect(mButtonBox, SIGNAL(rejected()), this, SLOT(reject())); - connect(mLineEdit, SIGNAL(textChanged(QString)), this, SLOT(slotUpdateOkButton(QString))); +} +Launcher::TextInputDialog::~TextInputDialog() +{ } int Launcher::TextInputDialog::exec() @@ -52,36 +54,18 @@ int Launcher::TextInputDialog::exec() return QDialog::exec(); } -QString Launcher::TextInputDialog::getText() const +void Launcher::TextInputDialog::setOkButtonEnabled(bool enabled) { - return mLineEdit->text(); -} + QPushButton *okButton = mButtonBox->button(QDialogButtonBox::Ok); + okButton->setEnabled(enabled); -void Launcher::TextInputDialog::slotUpdateOkButton(QString text) -{ - bool enabled = !(text.isEmpty()); - mButtonBox->button(QDialogButtonBox::Ok)->setEnabled(enabled); + QPalette palette; + palette.setColor(QPalette::Text, Qt::red); - if (enabled) + if (enabled) { mLineEdit->setPalette(QApplication::palette()); - else - { + } else { // Existing profile name, make the text red - QPalette *palette = new QPalette(); - palette->setColor(QPalette::Text,Qt::red); - mLineEdit->setPalette(*palette); + mLineEdit->setPalette(palette); } } - -Launcher::TextInputDialog::DialogLineEdit::DialogLineEdit (QWidget *parent) : - LineEdit (parent) -{ - int frameWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth); - - setObjectName(QString("LineEdit")); - setStyleSheet(QString("LineEdit { padding-right: %1px; } ").arg(mClearButton->sizeHint().width() + frameWidth + 1)); - QSize msz = minimumSizeHint(); - setMinimumSize(qMax(msz.width(), mClearButton->sizeHint().height() + frameWidth * 2 + 2), - qMax(msz.height(), mClearButton->sizeHint().height() + frameWidth * 2 + 2)); - -} diff --git a/apps/launcher/utils/textinputdialog.hpp b/apps/launcher/utils/textinputdialog.hpp index bb01778be..9eb9c717d 100644 --- a/apps/launcher/utils/textinputdialog.hpp +++ b/apps/launcher/utils/textinputdialog.hpp @@ -13,26 +13,20 @@ namespace Launcher { Q_OBJECT - class DialogLineEdit : public LineEdit - { - public: - explicit DialogLineEdit (QWidget *parent = 0); - }; - - DialogLineEdit *mLineEdit; - QDialogButtonBox *mButtonBox; - public: explicit TextInputDialog(const QString& title, const QString &text, QWidget *parent = 0); - ~TextInputDialog () {} + ~TextInputDialog (); - QString getText() const; + inline LineEdit *lineEdit() { return mLineEdit; } + void setOkButtonEnabled(bool enabled); int exec(); - private slots: - void slotUpdateOkButton(QString text); + private: + + QDialogButtonBox *mButtonBox; + LineEdit *mLineEdit; }; } diff --git a/apps/mwiniimporter/CMakeLists.txt b/apps/mwiniimporter/CMakeLists.txt index deab88ce2..790d47dc4 100644 --- a/apps/mwiniimporter/CMakeLists.txt +++ b/apps/mwiniimporter/CMakeLists.txt @@ -9,16 +9,16 @@ set(MWINIIMPORT_HEADER source_group(launcher FILES ${MWINIIMPORT} ${MWINIIMPORT_HEADER}) -add_executable(mwiniimport +add_executable(openmw-iniimporter ${MWINIIMPORT} ) -target_link_libraries(mwiniimport +target_link_libraries(openmw-iniimporter ${Boost_LIBRARIES} components ) if (BUILD_WITH_CODE_COVERAGE) add_definitions (--coverage) - target_link_libraries(mwiniimport gcov) + target_link_libraries(openmw-iniimporter gcov) endif() diff --git a/apps/mwiniimporter/importer.cpp b/apps/mwiniimporter/importer.cpp index 3a52592ae..efebe5a4d 100644 --- a/apps/mwiniimporter/importer.cpp +++ b/apps/mwiniimporter/importer.cpp @@ -8,13 +8,15 @@ #include #include -#include +#include +#include #include namespace bfs = boost::filesystem; MwIniImporter::MwIniImporter() : mVerbose(false) + , mEncoding(ToUTF8::WINDOWS_1250) { const char *map[][2] = { @@ -659,7 +661,7 @@ std::string MwIniImporter::numberToString(int n) { return str.str(); } -MwIniImporter::multistrmap MwIniImporter::loadIniFile(const std::string& filename) const { +MwIniImporter::multistrmap MwIniImporter::loadIniFile(const boost::filesystem::path& filename) const { std::cout << "load ini file: " << filename << std::endl; std::string section(""); @@ -709,8 +711,7 @@ MwIniImporter::multistrmap MwIniImporter::loadIniFile(const std::string& filenam continue; } - multistrmap::iterator it; - if((it = map.find(key)) == map.end()) { + if(map.find(key) == map.end()) { map.insert( std::make_pair (key, std::vector() ) ); } map[key].push_back(value); @@ -719,7 +720,7 @@ MwIniImporter::multistrmap MwIniImporter::loadIniFile(const std::string& filenam return map; } -MwIniImporter::multistrmap MwIniImporter::loadCfgFile(const std::string& filename) { +MwIniImporter::multistrmap MwIniImporter::loadCfgFile(const boost::filesystem::path& filename) { std::cout << "load cfg file: " << filename << std::endl; MwIniImporter::multistrmap map; @@ -746,8 +747,7 @@ MwIniImporter::multistrmap MwIniImporter::loadCfgFile(const std::string& filenam std::string key(line.substr(0,pos)); std::string value(line.substr(pos+1)); - multistrmap::iterator it; - if((it = map.find(key)) == map.end()) { + if(map.find(key) == map.end()) { map.insert( std::make_pair (key, std::vector() ) ); } map[key].push_back(value); @@ -826,10 +826,14 @@ void MwIniImporter::importArchives(multistrmap &cfg, const multistrmap &ini) con } } -void MwIniImporter::importGameFiles(multistrmap &cfg, const multistrmap &ini) const { - std::vector contentFiles; +void MwIniImporter::importGameFiles(multistrmap &cfg, const multistrmap &ini, const boost::filesystem::path& iniFilename) const { + std::vector > contentFiles; std::string baseGameFile("Game Files:GameFile"); std::string gameFile(""); + std::time_t defaultTime = 0; + + // assume the Game Files are all in a "Data Files" directory under the directory holding Morrowind.ini + const boost::filesystem::path gameFilesDir(iniFilename.parent_path() /= "Data Files"); multistrmap::const_iterator it = ini.begin(); for(int i=0; it != ini.end(); i++) { @@ -846,18 +850,20 @@ void MwIniImporter::importGameFiles(multistrmap &cfg, const multistrmap &ini) co Misc::StringUtils::toLower(filetype); if(filetype.compare("esm") == 0 || filetype.compare("esp") == 0) { - contentFiles.push_back(*entry); + boost::filesystem::path filepath(gameFilesDir); + filepath /= *entry; + contentFiles.push_back(std::make_pair(lastWriteTime(filepath, defaultTime), *entry)); } } - - gameFile = ""; } cfg.erase("content"); cfg.insert( std::make_pair("content", std::vector() ) ); - for(std::vector::const_iterator it=contentFiles.begin(); it!=contentFiles.end(); ++it) { - cfg["content"].push_back(*it); + // this will sort files by time order first, then alphabetical (maybe), I suspect non ASCII filenames will be stuffed. + sort(contentFiles.begin(), contentFiles.end()); + for(std::vector >::const_iterator it=contentFiles.begin(); it!=contentFiles.end(); ++it) { + cfg["content"].push_back(it->second); } } @@ -874,3 +880,27 @@ void MwIniImporter::setInputEncoding(const ToUTF8::FromType &encoding) { mEncoding = encoding; } + +std::time_t MwIniImporter::lastWriteTime(const boost::filesystem::path& filename, std::time_t defaultTime) +{ + std::time_t writeTime(defaultTime); + if (boost::filesystem::exists(filename)) + { + // FixMe: remove #if when Boost dependency for Linux builds updated + // This allows Linux to build until then +#if (BOOST_VERSION >= 104800) + // need to resolve any symlinks so that we get time of file, not symlink + boost::filesystem::path resolved = boost::filesystem::canonical(filename); +#else + boost::filesystem::path resolved = filename; +#endif + writeTime = boost::filesystem::last_write_time(resolved); + std::cout << "content file: " << resolved << " timestamp = (" << writeTime << + ") " << asctime(localtime(&writeTime)) << std::endl; + } + else + { + std::cout << "content file: " << filename << " not found" << std::endl; + } + return writeTime; +} \ No newline at end of file diff --git a/apps/mwiniimporter/importer.hpp b/apps/mwiniimporter/importer.hpp index 72b14ba75..c73cc65b5 100644 --- a/apps/mwiniimporter/importer.hpp +++ b/apps/mwiniimporter/importer.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include @@ -17,17 +18,22 @@ class MwIniImporter { MwIniImporter(); void setInputEncoding(const ToUTF8::FromType& encoding); void setVerbose(bool verbose); - multistrmap loadIniFile(const std::string& filename) const; - static multistrmap loadCfgFile(const std::string& filename); + multistrmap loadIniFile(const boost::filesystem::path& filename) const; + static multistrmap loadCfgFile(const boost::filesystem::path& filename); void merge(multistrmap &cfg, const multistrmap &ini) const; void mergeFallback(multistrmap &cfg, const multistrmap &ini) const; - void importGameFiles(multistrmap &cfg, const multistrmap &ini) const; + void importGameFiles(multistrmap &cfg, const multistrmap &ini, + const boost::filesystem::path& iniFilename) const; void importArchives(multistrmap &cfg, const multistrmap &ini) const; static void writeToFile(std::ostream &out, const multistrmap &cfg); private: static void insertMultistrmap(multistrmap &cfg, const std::string& key, const std::string& value); static std::string numberToString(int n); + + /// \return file's "last modified time", used in original MW to determine plug-in load order + static std::time_t lastWriteTime(const boost::filesystem::path& filename, std::time_t defaultTime); + bool mVerbose; strmap mMergeMap; std::vector mMergeFallback; diff --git a/apps/mwiniimporter/main.cpp b/apps/mwiniimporter/main.cpp index c10103cd6..3c48fedc9 100644 --- a/apps/mwiniimporter/main.cpp +++ b/apps/mwiniimporter/main.cpp @@ -37,6 +37,8 @@ public: char **get() const { return const_cast(argv); } private: + utf8argv(const utf8argv&); + utf8argv& operator=(const utf8argv&); const char **argv; std::vector args; @@ -54,93 +56,87 @@ int wmain(int argc, wchar_t *wargv[]) { char **argv = converter.get(); boost::filesystem::path::imbue(boost::locale::generator().generate("")); #endif - bpo::options_description desc("Syntax: mwiniimporter inifile configfile\nAllowed options"); - bpo::positional_options_description p_desc; - desc.add_options() - ("help,h", "produce help message") - ("verbose,v", "verbose output") - ("ini,i", bpo::value(), "morrowind.ini file") - ("cfg,c", bpo::value(), "openmw.cfg file") - ("output,o", bpo::value()->default_value(""), "openmw.cfg file") - ("game-files,g", "import esm and esp files") - ("no-archives,A", "disable bsa archives import") - ("encoding,e", bpo::value()-> default_value("win1252"), - "Character encoding used in OpenMW game messages:\n" - "\n\twin1250 - Central and Eastern European such as Polish, Czech, Slovak, Hungarian, Slovene, Bosnian, Croatian, Serbian (Latin script), Romanian and Albanian languages\n" - "\n\twin1251 - Cyrillic alphabet such as Russian, Bulgarian, Serbian Cyrillic and other languages\n" - "\n\twin1252 - Western European (Latin) alphabet, used by default") - ; - p_desc.add("ini", 1).add("cfg", 1); - - bpo::variables_map vm; - + try { + bpo::options_description desc("Syntax: openmw-iniimporter inifile configfile\nAllowed options"); + bpo::positional_options_description p_desc; + desc.add_options() + ("help,h", "produce help message") + ("verbose,v", "verbose output") + ("ini,i", bpo::value(), "morrowind.ini file") + ("cfg,c", bpo::value(), "openmw.cfg file") + ("output,o", bpo::value()->default_value(""), "openmw.cfg file") + ("game-files,g", "import esm and esp files") + ("no-archives,A", "disable bsa archives import") + ("encoding,e", bpo::value()-> default_value("win1252"), + "Character encoding used in OpenMW game messages:\n" + "\n\twin1250 - Central and Eastern European such as Polish, Czech, Slovak, Hungarian, Slovene, Bosnian, Croatian, Serbian (Latin script), Romanian and Albanian languages\n" + "\n\twin1251 - Cyrillic alphabet such as Russian, Bulgarian, Serbian Cyrillic and other languages\n" + "\n\twin1252 - Western European (Latin) alphabet, used by default") + ; + p_desc.add("ini", 1).add("cfg", 1); + + bpo::variables_map vm; + bpo::parsed_options parsed = bpo::command_line_parser(argc, argv) .options(desc) .positional(p_desc) .run(); bpo::store(parsed, vm); - } - catch(boost::program_options::unknown_option & x) - { - std::cerr << "ERROR: " << x.what() << std::endl; - return false; - } - catch(boost::program_options::invalid_command_line_syntax & x) - { - std::cerr << "ERROR: " << x.what() << std::endl; - return false; - } - - if(vm.count("help") || !vm.count("ini") || !vm.count("cfg")) { - std::cout << desc; - return 0; - } - bpo::notify(vm); + if(vm.count("help") || !vm.count("ini") || !vm.count("cfg")) { + std::cout << desc; + return 0; + } - std::string iniFile = vm["ini"].as(); - std::string cfgFile = vm["cfg"].as(); + bpo::notify(vm); - // if no output is given, write back to cfg file - std::string outputFile(vm["output"].as()); - if(vm["output"].defaulted()) { - outputFile = vm["cfg"].as(); - } + boost::filesystem::path iniFile(vm["ini"].as()); + boost::filesystem::path cfgFile(vm["cfg"].as()); - if(!boost::filesystem::exists(iniFile)) { - std::cerr << "ini file does not exist" << std::endl; - return -3; - } - if(!boost::filesystem::exists(cfgFile)) - std::cerr << "cfg file does not exist" << std::endl; + // if no output is given, write back to cfg file + std::string outputFile(vm["output"].as()); + if(vm["output"].defaulted()) { + outputFile = vm["cfg"].as(); + } - MwIniImporter importer; - importer.setVerbose(vm.count("verbose")); + if(!boost::filesystem::exists(iniFile)) { + std::cerr << "ini file does not exist" << std::endl; + return -3; + } + if(!boost::filesystem::exists(cfgFile)) + std::cerr << "cfg file does not exist" << std::endl; - // Font encoding settings - std::string encoding(vm["encoding"].as()); - importer.setInputEncoding(ToUTF8::calculateEncoding(encoding)); + MwIniImporter importer; + importer.setVerbose(vm.count("verbose")); - MwIniImporter::multistrmap ini = importer.loadIniFile(iniFile); - MwIniImporter::multistrmap cfg = importer.loadCfgFile(cfgFile); + // Font encoding settings + std::string encoding(vm["encoding"].as()); + importer.setInputEncoding(ToUTF8::calculateEncoding(encoding)); - importer.merge(cfg, ini); - importer.mergeFallback(cfg, ini); + MwIniImporter::multistrmap ini = importer.loadIniFile(iniFile); + MwIniImporter::multistrmap cfg = importer.loadCfgFile(cfgFile); - if(vm.count("game-files")) { - importer.importGameFiles(cfg, ini); - } + importer.merge(cfg, ini); + importer.mergeFallback(cfg, ini); - if(!vm.count("no-archives")) { - importer.importArchives(cfg, ini); - } + if(vm.count("game-files")) { + importer.importGameFiles(cfg, ini, iniFile); + } - std::cout << "write to: " << outputFile << std::endl; - bfs::ofstream file((bfs::path(outputFile))); - importer.writeToFile(file, cfg); + if(!vm.count("no-archives")) { + importer.importArchives(cfg, ini); + } + std::cout << "write to: " << outputFile << std::endl; + bfs::ofstream file((bfs::path(outputFile))); + importer.writeToFile(file, cfg); + } + catch (std::exception& e) + { + std::cerr << "ERROR: " << e.what() << std::endl; + } return 0; } diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt index 6a116151c..ab3765bb6 100644 --- a/apps/opencs/CMakeLists.txt +++ b/apps/opencs/CMakeLists.txt @@ -1,15 +1,15 @@ -set (OPENCS_SRC main.cpp) +set (OPENCS_SRC main.cpp + ${CMAKE_SOURCE_DIR}/files/windows/opencs.rc + ) opencs_units (. editor) -set (CMAKE_BUILD_TYPE DEBUG) - opencs_units (model/doc - document operation saving documentmanager loader + document operation saving documentmanager loader runner ) opencs_units_noqt (model/doc - stage savingstate savingstages + stage savingstate savingstages blacklist messages ) opencs_hdrs_noqt (model/doc @@ -24,13 +24,13 @@ opencs_units (model/world opencs_units_noqt (model/world - nestedtablewrapper universalid record commands columnbase scriptcontext cell refidcollection - refidadapter refiddata refidadapterimp ref collectionbase refcollection columns infocollection - tablemimedata cellcoordinates cellselection resources resourcesmanager nestedadaptors + universalid record commands columnbase scriptcontext cell refidcollection + refidadapter refiddata refidadapterimp ref collectionbase refcollection columns infocollection tablemimedata cellcoordinates cellselection resources resourcesmanager scope + pathgrid landtexture land nestedtablewrapper nestedadaptors ) opencs_hdrs_noqt (model/world - columnimp idcollection collection info + columnimp idcollection collection info subcellcollection ) @@ -40,13 +40,13 @@ opencs_units (model/tools opencs_units_noqt (model/tools mandatoryid skillcheck classcheck factioncheck racecheck soundcheck regioncheck - birthsigncheck spellcheck referenceablecheck scriptcheck + birthsigncheck spellcheck referencecheck referenceablecheck scriptcheck bodypartcheck ) opencs_units (view/doc viewmanager view operations operation subview startup filedialog newgame - filewidget adjusterwidget loader + filewidget adjusterwidget loader globaldebugprofilemenu runlogsubview ) @@ -67,25 +67,31 @@ opencs_units (view/world opencs_units_noqt (view/world subviews enumdelegate vartypedelegate recordstatusdelegate idtypedelegate datadisplaydelegate - scripthighlighter idvalidator dialoguecreator + scripthighlighter idvalidator dialoguecreator physicssystem + ) + +opencs_units (view/widget + scenetoolbar scenetool scenetoolmode pushbutton scenetooltoggle scenetoolrun modebutton + scenetooltoggle2 ) opencs_units (view/render scenewidget worldspacewidget pagedworldspacewidget unpagedworldspacewidget - previewwidget + previewwidget editmode ) opencs_units_noqt (view/render navigation navigation1st navigationfree navigationorbit lighting lightingday lightingnight - lightingbright object cell + lightingbright object cell terrainstorage textoverlay overlaymask overlaysystem mousestate ) -opencs_units (view/widget - scenetoolbar scenetool scenetoolmode pushbutton +opencs_hdrs_noqt (view/render + elements ) + opencs_units (view/tools - reportsubview + reportsubview reporttable ) opencs_units_noqt (view/tools @@ -123,12 +129,8 @@ opencs_units_noqt (model/filter node unarynode narynode leafnode booleannode parser andnode ornode notnode textnode valuenode ) -opencs_hdrs_noqt (model/filter - filter - ) - opencs_units (view/filter - filtercreator filterbox recordfilterbox editwidget + filterbox recordfilterbox editwidget ) set (OPENCS_US @@ -143,7 +145,7 @@ set (OPENCS_UI ${CMAKE_SOURCE_DIR}/files/ui/filedialog.ui ) -source_group (opencs FILES ${OPENCS_SRC} ${OPENCS_HDR}) +source_group (openmw-cs FILES ${OPENCS_SRC} ${OPENCS_HDR}) if(WIN32) set(QT_USE_QTMAIN TRUE) @@ -163,16 +165,17 @@ qt4_wrap_ui(OPENCS_UI_HDR ${OPENCS_UI}) qt4_wrap_cpp(OPENCS_MOC_SRC ${OPENCS_HDR_QT}) qt4_add_resources(OPENCS_RES_SRC ${OPENCS_RES}) -include_directories(${CMAKE_CURRENT_BINARY_DIR}) +include_directories(${CMAKE_CURRENT_BINARY_DIR} ${BULLET_INCLUDE_DIRS}) if(APPLE) - set (OPENCS_MAC_ICON ${CMAKE_SOURCE_DIR}/files/mac/opencs.icns) + set (OPENCS_MAC_ICON ${CMAKE_SOURCE_DIR}/files/mac/openmw-cs.icns) else() set (OPENCS_MAC_ICON "") endif(APPLE) -add_executable(opencs +add_executable(openmw-cs MACOSX_BUNDLE + ${OENGINE_BULLET} ${OPENCS_SRC} ${OPENCS_UI_HDR} ${OPENCS_MOC_SRC} @@ -181,10 +184,10 @@ add_executable(opencs ) if(APPLE) - set_target_properties(opencs PROPERTIES + set_target_properties(openmw-cs PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${OpenMW_BINARY_DIR}" - OUTPUT_NAME "OpenCS" - MACOSX_BUNDLE_ICON_FILE "opencs.icns" + OUTPUT_NAME "OpenMW-CS" + MACOSX_BUNDLE_ICON_FILE "openmw-cs.icns" MACOSX_BUNDLE_BUNDLE_NAME "OpenCS" MACOSX_BUNDLE_GUI_IDENTIFIER "org.openmw.opencs" MACOSX_BUNDLE_SHORT_VERSION_STRING ${OPENMW_VERSION} @@ -195,15 +198,17 @@ if(APPLE) MACOSX_PACKAGE_LOCATION Resources) endif(APPLE) -target_link_libraries(opencs +target_link_libraries(openmw-cs ${OGRE_LIBRARIES} + ${OGRE_Overlay_LIBRARIES} ${OGRE_STATIC_PLUGINS} ${SHINY_LIBRARIES} ${Boost_LIBRARIES} + ${BULLET_LIBRARIES} ${QT_LIBRARIES} components ) if(APPLE) - INSTALL(TARGETS opencs BUNDLE DESTINATION OpenMW COMPONENT BUNDLE) + INSTALL(TARGETS openmw-cs BUNDLE DESTINATION OpenMW COMPONENT BUNDLE) endif() diff --git a/apps/opencs/editor.cpp b/apps/opencs/editor.cpp index b3513a7f1..1d31c8396 100644 --- a/apps/opencs/editor.cpp +++ b/apps/opencs/editor.cpp @@ -1,6 +1,8 @@ #include "editor.hpp" +#include + #include #include #include @@ -13,15 +15,16 @@ #include #include - +#include #include #include "model/doc/document.hpp" #include "model/world/data.hpp" CS::Editor::Editor (OgreInit::OgreInit& ogreInit) -: mUserSettings (mCfgMgr), mDocumentManager (mCfgMgr), mViewManager (mDocumentManager), - mIpcServerName ("org.openmw.OpenCS") +: mUserSettings (mCfgMgr), mOverlaySystem (0), mDocumentManager (mCfgMgr), + mViewManager (mDocumentManager), + mIpcServerName ("org.openmw.OpenCS"), mServer(NULL), mClientSocket(NULL), mPid(""), mLock() { std::pair > config = readConfig(); @@ -32,6 +35,10 @@ CS::Editor::Editor (OgreInit::OgreInit& ogreInit) ogreInit.init ((mCfgMgr.getUserConfigPath() / "opencsOgre.log").string()); + NifOgre::Loader::setShowMarkers(true); + + mOverlaySystem.reset (new CSVRender::OverlaySystem); + Bsa::registerResources (Files::Collections (config.first, !mFsStrict), config.second, true, mFsStrict); @@ -65,6 +72,18 @@ CS::Editor::Editor (OgreInit::OgreInit& ogreInit) this, SLOT (createNewGame (const boost::filesystem::path&))); } +CS::Editor::~Editor () +{ + mPidFile.close(); + + if(mServer && boost::filesystem::exists(mPid)) + static_cast ( // silence coverity warning + remove(mPid.string().c_str())); // ignore any error + + // cleanup global resources used by OEngine + delete OEngine::Physic::BulletShapeManager::getSingletonPtr(); +} + void CS::Editor::setupDataFiles (const Files::PathContainer& dataDirs) { for (Files::PathContainer::const_iterator iter = dataDirs.begin(); iter != dataDirs.end(); ++iter) @@ -77,16 +96,20 @@ void CS::Editor::setupDataFiles (const Files::PathContainer& dataDirs) std::pair > CS::Editor::readConfig() { boost::program_options::variables_map variables; - boost::program_options::options_description desc("Syntax: opencs \nAllowed options"); + boost::program_options::options_description desc("Syntax: openmw-cs \nAllowed options"); desc.add_options() - ("data", boost::program_options::value()->default_value(Files::PathContainer(), "data")->multitoken()) + ("data", boost::program_options::value()->default_value(Files::PathContainer(), "data")->multitoken()->composing()) ("data-local", boost::program_options::value()->default_value("")) ("fs-strict", boost::program_options::value()->implicit_value(true)->default_value(false)) ("encoding", boost::program_options::value()->default_value("win1252")) ("resources", boost::program_options::value()->default_value("resources")) ("fallback-archive", boost::program_options::value >()-> - default_value(std::vector(), "fallback-archive")->multitoken()); + default_value(std::vector(), "fallback-archive")->multitoken()) + ("script-blacklist", boost::program_options::value >()->default_value(std::vector(), "") + ->multitoken(), "exclude specified script from the verifier (if the use of the blacklist is enabled)") + ("script-blacklist-use", boost::program_options::value()->implicit_value(true) + ->default_value(true), "enable script blacklisting"); boost::program_options::notify(variables); @@ -97,6 +120,10 @@ std::pair > CS::Editor::readConfi mDocumentManager.setResourceDir (mResources = variables["resources"].as()); + if (variables["script-blacklist-use"].as()) + mDocumentManager.setBlacklistedScripts ( + variables["script-blacklist"].as >()); + mFsStrict = variables["fs-strict"].as(); Files::PathContainer dataDirs, dataLocal; @@ -181,7 +208,7 @@ void CS::Editor::createNewFile (const boost::filesystem::path &savePath) files.push_back(path.toUtf8().constData()); } - files.push_back(mFileDialog.filename().toUtf8().constData()); + files.push_back (savePath); mDocumentManager.addDocument (files, savePath, true); @@ -212,13 +239,60 @@ void CS::Editor::showSettings() if (mSettings.isHidden()) mSettings.show(); + mSettings.move (QCursor::pos()); mSettings.raise(); mSettings.activateWindow(); } bool CS::Editor::makeIPCServer() { - mServer = new QLocalServer(this); + try + { + mPid = boost::filesystem::temp_directory_path(); + mPid /= "openmw-cs.pid"; + bool pidExists = boost::filesystem::exists(mPid); + + mPidFile.open(mPid); + + mLock = boost::interprocess::file_lock(mPid.string().c_str()); + if(!mLock.try_lock()) + { + std::cerr << "OpenCS already running." << std::endl; + return false; + } + +#ifdef _WIN32 + mPidFile << GetCurrentProcessId() << std::endl; +#else + mPidFile << getpid() << std::endl; +#endif + + mServer = new QLocalServer(this); + + if(pidExists) + { + // hack to get the temp directory path + mServer->listen("dummy"); + QString fullPath = mServer->fullServerName(); + mServer->close(); + fullPath.remove(QRegExp("dummy$")); + fullPath += mIpcServerName; + if(boost::filesystem::exists(fullPath.toStdString().c_str())) + { + // TODO: compare pid of the current process with that in the file + std::cout << "Detected unclean shutdown." << std::endl; + // delete the stale file + if(remove(fullPath.toStdString().c_str())) + std::cerr << "ERROR removing stale connection file" << std::endl; + } + } + } + + catch(const std::exception& e) + { + std::cerr << "ERROR " << e.what() << std::endl; + return false; + } if(mServer->listen(mIpcServerName)) { @@ -227,6 +301,7 @@ bool CS::Editor::makeIPCServer() } mServer->close(); + mServer = NULL; return false; } @@ -251,26 +326,49 @@ int CS::Editor::run() std::auto_ptr CS::Editor::setupGraphics() { - // TODO: setting - Ogre::Root::getSingleton().setRenderSystem(Ogre::Root::getSingleton().getRenderSystemByName("OpenGL Rendering Subsystem")); + std::string renderer = +#if OGRE_PLATFORM == OGRE_PLATFORM_WIN32 + "Direct3D9 Rendering Subsystem"; +#else + "OpenGL Rendering Subsystem"; +#endif + std::string renderSystem = mUserSettings.setting("Video/render system", renderer.c_str()).toStdString(); + + Ogre::Root::getSingleton().setRenderSystem(Ogre::Root::getSingleton().getRenderSystemByName(renderSystem)); + + // Initialise Ogre::OverlaySystem after Ogre::Root but before initialisation + mOverlaySystem.get(); Ogre::Root::getSingleton().initialise(false); // Create a hidden background window to keep resources Ogre::NameValuePairList params; params.insert(std::make_pair("title", "")); - params.insert(std::make_pair("FSAA", "0")); + + std::string antialiasing = mUserSettings.settingValue("Video/antialiasing").toStdString(); + if(antialiasing == "MSAA 16") antialiasing = "16"; + else if(antialiasing == "MSAA 8") antialiasing = "8"; + else if(antialiasing == "MSAA 4") antialiasing = "4"; + else if(antialiasing == "MSAA 2") antialiasing = "2"; + else antialiasing = "0"; + params.insert(std::make_pair("FSAA", antialiasing)); + params.insert(std::make_pair("vsync", "false")); params.insert(std::make_pair("hidden", "true")); #if OGRE_PLATFORM == OGRE_PLATFORM_APPLE params.insert(std::make_pair("macAPI", "cocoa")); #endif + // NOTE: fullscreen mode not supported (doesn't really make sense for opencs) Ogre::RenderWindow* hiddenWindow = Ogre::Root::getSingleton().createRenderWindow("InactiveHidden", 1, 1, false, ¶ms); hiddenWindow->setActive(false); sh::OgrePlatform* platform = new sh::OgrePlatform ("General", (mResources / "materials").string()); + // for font used in overlays + Ogre::Root::getSingleton().addResourceLocation ((mResources / "mygui").string(), + "FileSystem", Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, true); + if (!boost::filesystem::exists (mCfgMgr.getCachePath())) boost::filesystem::create_directories (mCfgMgr.getCachePath()); @@ -278,7 +376,28 @@ std::auto_ptr CS::Editor::setupGraphics() std::auto_ptr factory (new sh::Factory (platform)); - factory->setCurrentLanguage (sh::Language_GLSL); /// \todo make this configurable + QString shLang = mUserSettings.settingValue("General/shader mode"); + QString rend = renderSystem.c_str(); + bool openGL = rend.contains(QRegExp("^OpenGL", Qt::CaseInsensitive)); + bool glES = rend.contains(QRegExp("^OpenGL ES", Qt::CaseInsensitive)); + + // force shader language based on render system + if(shLang == "" + || (openGL && shLang == "hlsl") + || (!openGL && shLang == "glsl") + || (glES && shLang != "glsles")) + { + shLang = openGL ? (glES ? "glsles" : "glsl") : "hlsl"; + //no group means "General" group in the "ini" file standard + mUserSettings.setDefinitions("shader mode", (QStringList() << shLang)); + } + enum sh::Language lang; + if(shLang == "glsl") lang = sh::Language_GLSL; + else if(shLang == "glsles") lang = sh::Language_GLSLES; + else if(shLang == "hlsl") lang = sh::Language_HLSL; + else lang = sh::Language_CG; + + factory->setCurrentLanguage (lang); factory->setWriteSourceCache (true); factory->setReadSourceCache (true); factory->setReadMicrocodeCache (true); @@ -286,16 +405,27 @@ std::auto_ptr CS::Editor::setupGraphics() factory->loadAllFiles(); - sh::Factory::getInstance().setGlobalSetting ("fog", "true"); + bool shaders = mUserSettings.setting("3d-render/shaders", QString("true")) == "true" ? true : false; + sh::Factory::getInstance ().setShadersEnabled (shaders); + + std::string fog = mUserSettings.setting("Shader/fog", QString("true")).toStdString(); + sh::Factory::getInstance().setGlobalSetting ("fog", fog); + + + std::string shadows = mUserSettings.setting("Shader/shadows", QString("false")).toStdString(); + sh::Factory::getInstance().setGlobalSetting ("shadows", shadows); - sh::Factory::getInstance().setGlobalSetting ("shadows", "false"); - sh::Factory::getInstance().setGlobalSetting ("shadows_pssm", "false"); + std::string shadows_pssm = mUserSettings.setting("Shader/shadows_pssm", QString("false")).toStdString(); + sh::Factory::getInstance().setGlobalSetting ("shadows_pssm", shadows_pssm); - sh::Factory::getInstance ().setGlobalSetting ("render_refraction", "false"); + std::string render_refraction = mUserSettings.setting("Shader/render_refraction", QString("false")).toStdString(); + sh::Factory::getInstance ().setGlobalSetting ("render_refraction", render_refraction); + // internal setting - may be switched on or off by the use of shader configurations sh::Factory::getInstance ().setGlobalSetting ("viewproj_fix", "false"); - sh::Factory::getInstance ().setGlobalSetting ("num_lights", "8"); + std::string num_lights = mUserSettings.setting("3d-render-adv/num_lights", QString("8")).toStdString(); + sh::Factory::getInstance ().setGlobalSetting ("num_lights", num_lights); /// \todo add more configurable shiny settings @@ -309,5 +439,5 @@ void CS::Editor::documentAdded (CSMDoc::Document *document) void CS::Editor::lastDocumentDeleted() { - exit (0); + QApplication::quit(); } diff --git a/apps/opencs/editor.hpp b/apps/opencs/editor.hpp index d88da9865..273f0825b 100644 --- a/apps/opencs/editor.hpp +++ b/apps/opencs/editor.hpp @@ -3,6 +3,9 @@ #include +#include +#include + #include #include #include @@ -16,6 +19,8 @@ #include +#include + #include "model/settings/usersettings.hpp" #include "model/doc/documentmanager.hpp" @@ -25,6 +30,7 @@ #include "view/doc/newgame.hpp" #include "view/settings/dialog.hpp" +#include "view/render/overlaysystem.hpp" namespace OgreInit { @@ -37,8 +43,10 @@ namespace CS { Q_OBJECT + Nif::Cache mNifCache; Files::ConfigurationManager mCfgMgr; CSMSettings::UserSettings mUserSettings; + std::auto_ptr mOverlaySystem; CSMDoc::DocumentManager mDocumentManager; CSVDoc::ViewManager mViewManager; CSVDoc::StartupDialogue mStartup; @@ -47,6 +55,9 @@ namespace CS CSVDoc::FileDialog mFileDialog; boost::filesystem::path mLocal; boost::filesystem::path mResources; + boost::filesystem::path mPid; + boost::interprocess::file_lock mLock; + boost::filesystem::ofstream mPidFile; bool mFsStrict; void setupDataFiles (const Files::PathContainer& dataDirs); @@ -61,6 +72,7 @@ namespace CS public: Editor (OgreInit::OgreInit& ogreInit); + ~Editor (); bool makeIPCServer(); void connectToIPCServer(); diff --git a/apps/opencs/main.cpp b/apps/opencs/main.cpp index b184a1ef1..b11561c13 100644 --- a/apps/opencs/main.cpp +++ b/apps/opencs/main.cpp @@ -46,47 +46,55 @@ class Application : public QApplication int main(int argc, char *argv[]) { - Q_INIT_RESOURCE (resources); + try + { + Q_INIT_RESOURCE (resources); - qRegisterMetaType ("std::string"); - qRegisterMetaType ("CSMWorld::UniversalId"); + qRegisterMetaType ("std::string"); + qRegisterMetaType ("CSMWorld::UniversalId"); - OgreInit::OgreInit ogreInit; + OgreInit::OgreInit ogreInit; - std::auto_ptr shinyFactory; + std::auto_ptr shinyFactory; - Application application (argc, argv); + Application application (argc, argv); -#ifdef Q_OS_MAC - QDir dir(QCoreApplication::applicationDirPath()); - if (dir.dirName() == "MacOS") { - dir.cdUp(); - dir.cdUp(); - dir.cdUp(); - } - QDir::setCurrent(dir.absolutePath()); + #ifdef Q_OS_MAC + QDir dir(QCoreApplication::applicationDirPath()); + if (dir.dirName() == "MacOS") { + dir.cdUp(); + dir.cdUp(); + dir.cdUp(); + } + QDir::setCurrent(dir.absolutePath()); - // force Qt to load only LOCAL plugins, don't touch system Qt installation - QDir pluginsPath(QCoreApplication::applicationDirPath()); - pluginsPath.cdUp(); - pluginsPath.cd("Plugins"); + // force Qt to load only LOCAL plugins, don't touch system Qt installation + QDir pluginsPath(QCoreApplication::applicationDirPath()); + pluginsPath.cdUp(); + pluginsPath.cd("Plugins"); - QStringList libraryPaths; - libraryPaths << pluginsPath.path() << QCoreApplication::applicationDirPath(); - application.setLibraryPaths(libraryPaths); -#endif + QStringList libraryPaths; + libraryPaths << pluginsPath.path() << QCoreApplication::applicationDirPath(); + application.setLibraryPaths(libraryPaths); + #endif - application.setWindowIcon (QIcon (":./opencs.png")); + application.setWindowIcon (QIcon (":./openmw-cs.png")); - CS::Editor editor (ogreInit); + CS::Editor editor (ogreInit); - if(!editor.makeIPCServer()) + if(!editor.makeIPCServer()) + { + editor.connectToIPCServer(); + return 0; + } + + shinyFactory = editor.setupGraphics(); + return editor.run(); + } + catch (std::exception& e) { - editor.connectToIPCServer(); - // return 0; + std::cerr << "ERROR: " << e.what() << std::endl; + return 0; } - shinyFactory = editor.setupGraphics(); - - return editor.run(); } diff --git a/apps/opencs/model/doc/blacklist.cpp b/apps/opencs/model/doc/blacklist.cpp new file mode 100644 index 000000000..9b37a4302 --- /dev/null +++ b/apps/opencs/model/doc/blacklist.cpp @@ -0,0 +1,31 @@ + +#include "blacklist.hpp" + +#include + +#include + +bool CSMDoc::Blacklist::isBlacklisted (const CSMWorld::UniversalId& id) const +{ + std::map >::const_iterator iter = + mIds.find (id.getType()); + + if (iter==mIds.end()) + return false; + + return std::binary_search (iter->second.begin(), iter->second.end(), + Misc::StringUtils::lowerCase (id.getId())); +} + +void CSMDoc::Blacklist::add (CSMWorld::UniversalId::Type type, + const std::vector& ids) +{ + std::vector& list = mIds[type]; + + int size = list.size(); + + list.resize (size+ids.size()); + + std::transform (ids.begin(), ids.end(), list.begin()+size, Misc::StringUtils::lowerCase); + std::sort (list.begin(), list.end()); +} \ No newline at end of file diff --git a/apps/opencs/model/doc/blacklist.hpp b/apps/opencs/model/doc/blacklist.hpp new file mode 100644 index 000000000..9bf7f1d86 --- /dev/null +++ b/apps/opencs/model/doc/blacklist.hpp @@ -0,0 +1,25 @@ +#ifndef CSM_DOC_BLACKLIST_H +#define CSM_DOC_BLACKLIST_H + +#include +#include +#include + +#include "../world/universalid.hpp" + +namespace CSMDoc +{ + /// \brief ID blacklist sorted by UniversalId type + class Blacklist + { + std::map > mIds; + + public: + + bool isBlacklisted (const CSMWorld::UniversalId& id) const; + + void add (CSMWorld::UniversalId::Type type, const std::vector& ids); + }; +} + +#endif diff --git a/apps/opencs/model/doc/document.cpp b/apps/opencs/model/doc/document.cpp index eb03be085..61fe4b322 100644 --- a/apps/opencs/model/doc/document.cpp +++ b/apps/opencs/model/doc/document.cpp @@ -10,6 +10,8 @@ #include #endif +#include "../../view/world/physicssystem.hpp" + void CSMDoc::Document::addGmsts() { static const char *gmstFloats[] = @@ -2024,6 +2026,7 @@ void CSMDoc::Document::addOptionalGmsts() { ESM::GameSetting gmst; gmst.mId = sFloats[i]; + gmst.blank(); gmst.mValue.setType (ESM::VT_Float); addOptionalGmst (gmst); } @@ -2032,6 +2035,7 @@ void CSMDoc::Document::addOptionalGmsts() { ESM::GameSetting gmst; gmst.mId = sIntegers[i]; + gmst.blank(); gmst.mValue.setType (ESM::VT_Int); addOptionalGmst (gmst); } @@ -2040,6 +2044,7 @@ void CSMDoc::Document::addOptionalGmsts() { ESM::GameSetting gmst; gmst.mId = sStrings[i]; + gmst.blank(); gmst.mValue.setType (ESM::VT_String); gmst.mValue.setString (""); addOptionalGmst (gmst); @@ -2060,6 +2065,7 @@ void CSMDoc::Document::addOptionalGlobals() { ESM::Global global; global.mId = sGlobals[i]; + global.blank(); global.mValue.setType (ESM::VT_Long); if (i==0) @@ -2069,6 +2075,19 @@ void CSMDoc::Document::addOptionalGlobals() } } +void CSMDoc::Document::addOptionalMagicEffects() +{ + for (int i=ESM::MagicEffect::SummonFabricant; i<=ESM::MagicEffect::SummonCreature05; ++i) + { + ESM::MagicEffect effect; + effect.mIndex = i; + effect.mId = ESM::MagicEffect::indexToId (i); + effect.blank(); + + addOptionalMagicEffect (effect); + } +} + void CSMDoc::Document::addOptionalGmst (const ESM::GameSetting& gmst) { if (getData().getGmsts().searchId (gmst.mId)==-1) @@ -2091,6 +2110,17 @@ void CSMDoc::Document::addOptionalGlobal (const ESM::Global& global) } } +void CSMDoc::Document::addOptionalMagicEffect (const ESM::MagicEffect& magicEffect) +{ + if (getData().getMagicEffects().searchId (magicEffect.mId)==-1) + { + CSMWorld::Record record; + record.mBase = magicEffect; + record.mState = CSMWorld::RecordBase::State_BaseOnly; + getData().getMagicEffects().appendRecord (record); + } +} + void CSMDoc::Document::createBase() { static const char *sGlobals[] = @@ -2202,17 +2232,31 @@ void CSMDoc::Document::createBase() getData().getTopics().add (record); } + + for (int i=0; i& files, bool new_, const boost::filesystem::path& savePath, const boost::filesystem::path& resDir, - ToUTF8::FromType encoding, const CSMWorld::ResourcesManager& resourcesManager) + ToUTF8::FromType encoding, const CSMWorld::ResourcesManager& resourcesManager, + const std::vector& blacklistedScripts) : mSavePath (savePath), mContentFiles (files), mNew (new_), mData (encoding, resourcesManager), - mTools (mData), mResDir(resDir), + mTools (*this), mResDir(resDir), mProjectPath ((configuration.getUserDataPath() / "projects") / (savePath.filename().string() + ".project")), - mSaving (*this, mProjectPath, encoding) + mSaving (*this, mProjectPath, encoding), + mRunner (mProjectPath), mPhysics(boost::shared_ptr()) { if (mContentFiles.empty()) throw std::runtime_error ("Empty content file sequence"); @@ -2222,14 +2266,15 @@ CSMDoc::Document::Document (const Files::ConfigurationManager& configuration, boost::filesystem::path customFiltersPath (configuration.getUserDataPath()); customFiltersPath /= "defaultfilters"; - std::ofstream dst(mProjectPath.c_str(), std::ios::binary); + std::ofstream destination (mProjectPath.string().c_str(), std::ios::binary); if (boost::filesystem::exists (customFiltersPath)) { - dst< contentFiles; + + for (std::vector::const_iterator iter (mContentFiles.begin()); + iter!=mContentFiles.end(); ++iter) + contentFiles.push_back (iter->filename().string()); + + mRunner.configure (getData().getDebugProfiles().getRecord (profile).get(), contentFiles, + startupInstruction); + + int state = getState(); + + if (state & State_Modified) + { + // need to save first + mRunner.start (true); + + new SaveWatcher (&mRunner, &mSaving); // no, that is not a memory leak. Qt is weird. + + if (!(state & State_Saving)) + save(); + } + else + mRunner.start(); +} + +void CSMDoc::Document::stopRunning() +{ + mRunner.stop(); +} + +QTextDocument *CSMDoc::Document::getRunLog() +{ + return mRunner.getLog(); +} + +void CSMDoc::Document::runStateChanged() +{ + emit stateChanged (getState(), this); +} + void CSMDoc::Document::progress (int current, int max, int type) { emit progress (current, max, type, 1, this); } + +boost::shared_ptr CSMDoc::Document::getPhysics () +{ + if(!mPhysics) + mPhysics = boost::shared_ptr (new CSVWorld::PhysicsSystem()); + + return mPhysics; +} diff --git a/apps/opencs/model/doc/document.hpp b/apps/opencs/model/doc/document.hpp index d092b47c8..f3aef6db6 100644 --- a/apps/opencs/model/doc/document.hpp +++ b/apps/opencs/model/doc/document.hpp @@ -3,6 +3,7 @@ #include +#include #include #include @@ -17,6 +18,8 @@ #include "state.hpp" #include "saving.hpp" +#include "blacklist.hpp" +#include "runner.hpp" class QAbstractItemModel; @@ -24,6 +27,7 @@ namespace ESM { struct GameSetting; struct Global; + struct MagicEffect; } namespace Files @@ -36,6 +40,11 @@ namespace CSMWorld class ResourcesManager; } +namespace CSVWorld +{ + class PhysicsSystem; +} + namespace CSMDoc { class Document : public QObject @@ -52,6 +61,9 @@ namespace CSMDoc boost::filesystem::path mProjectPath; Saving mSaving; boost::filesystem::path mResDir; + Blacklist mBlacklist; + Runner mRunner; + boost::shared_ptr mPhysics; // It is important that the undo stack is declared last, because on desctruction it fires a signal, that is connected to a slot, that is // using other member variables. Unfortunately this connection is cut only in the QObject destructor, which is way too late. @@ -69,16 +81,21 @@ namespace CSMDoc void addOptionalGlobals(); + void addOptionalMagicEffects(); + void addOptionalGmst (const ESM::GameSetting& gmst); void addOptionalGlobal (const ESM::Global& global); + void addOptionalMagicEffect (const ESM::MagicEffect& effect); + public: Document (const Files::ConfigurationManager& configuration, const std::vector< boost::filesystem::path >& files, bool new_, const boost::filesystem::path& savePath, const boost::filesystem::path& resDir, - ToUTF8::FromType encoding, const CSMWorld::ResourcesManager& resourcesManager); + ToUTF8::FromType encoding, const CSMWorld::ResourcesManager& resourcesManager, + const std::vector& blacklistedScripts); ~Document(); @@ -110,6 +127,17 @@ namespace CSMDoc CSMTools::ReportModel *getReport (const CSMWorld::UniversalId& id); ///< The ownership of the returned report is not transferred. + bool isBlacklisted (const CSMWorld::UniversalId& id) const; + + void startRunning (const std::string& profile, + const std::string& startupInstruction = ""); + + void stopRunning(); + + QTextDocument *getRunLog(); + + boost::shared_ptr getPhysics(); + signals: void stateChanged (int state, CSMDoc::Document *document); @@ -121,9 +149,11 @@ namespace CSMDoc void modificationStateChanged (bool clean); void reportMessage (const CSMWorld::UniversalId& id, const std::string& message, - int type); + const std::string& hint, int type); + + void operationDone (int type, bool failed); - void operationDone (int type); + void runStateChanged(); public slots: diff --git a/apps/opencs/model/doc/documentmanager.cpp b/apps/opencs/model/doc/documentmanager.cpp index 6953db0ed..9b807225c 100644 --- a/apps/opencs/model/doc/documentmanager.cpp +++ b/apps/opencs/model/doc/documentmanager.cpp @@ -52,7 +52,7 @@ CSMDoc::DocumentManager::~DocumentManager() void CSMDoc::DocumentManager::addDocument (const std::vector& files, const boost::filesystem::path& savePath, bool new_) { - Document *document = new Document (mConfiguration, files, new_, savePath, mResDir, mEncoding, mResourcesManager); + Document *document = new Document (mConfiguration, files, new_, savePath, mResDir, mEncoding, mResourcesManager, mBlacklistedScripts); mDocuments.push_back (document); @@ -85,6 +85,11 @@ void CSMDoc::DocumentManager::setEncoding (ToUTF8::FromType encoding) mEncoding = encoding; } +void CSMDoc::DocumentManager::setBlacklistedScripts (const std::vector& scriptIds) +{ + mBlacklistedScripts = scriptIds; +} + void CSMDoc::DocumentManager::listResources() { mResourcesManager.listResources(); diff --git a/apps/opencs/model/doc/documentmanager.hpp b/apps/opencs/model/doc/documentmanager.hpp index cebae6f6d..c545b9a9f 100644 --- a/apps/opencs/model/doc/documentmanager.hpp +++ b/apps/opencs/model/doc/documentmanager.hpp @@ -34,6 +34,7 @@ namespace CSMDoc Loader mLoader; ToUTF8::FromType mEncoding; CSMWorld::ResourcesManager mResourcesManager; + std::vector mBlacklistedScripts; DocumentManager (const DocumentManager&); DocumentManager& operator= (const DocumentManager&); @@ -53,6 +54,8 @@ namespace CSMDoc void setEncoding (ToUTF8::FromType encoding); + void setBlacklistedScripts (const std::vector& scriptIds); + /// Ask OGRE for a list of available resources. void listResources(); diff --git a/apps/opencs/model/doc/loader.cpp b/apps/opencs/model/doc/loader.cpp index 712deb9df..43f3b850e 100644 --- a/apps/opencs/model/doc/loader.cpp +++ b/apps/opencs/model/doc/loader.cpp @@ -52,7 +52,7 @@ void CSMDoc::Loader::load() { if (iter->second.mRecordsLeft) { - CSMDoc::Stage::Messages messages; + CSMDoc::Messages messages; for (int i=0; igetData().continueLoading (messages)) { @@ -65,11 +65,11 @@ void CSMDoc::Loader::load() CSMWorld::UniversalId log (CSMWorld::UniversalId::Type_LoadErrorLog, 0); { // silence a g++ warning - for (CSMDoc::Stage::Messages::const_iterator iter (messages.begin()); + for (CSMDoc::Messages::Iterator iter (messages.begin()); iter!=messages.end(); ++iter) { - document->getReport (log)->add (iter->first, iter->second); - emit loadMessage (document, iter->second); + document->getReport (log)->add (iter->mId, iter->mMessage); + emit loadMessage (document, iter->mMessage); } } diff --git a/apps/opencs/model/doc/messages.cpp b/apps/opencs/model/doc/messages.cpp new file mode 100644 index 000000000..1fb423145 --- /dev/null +++ b/apps/opencs/model/doc/messages.cpp @@ -0,0 +1,28 @@ + +#include "messages.hpp" + +void CSMDoc::Messages::add (const CSMWorld::UniversalId& id, const std::string& message, + const std::string& hint) +{ + Message data; + data.mId = id; + data.mMessage = message; + data.mHint = hint; + + mMessages.push_back (data); +} + +void CSMDoc::Messages::push_back (const std::pair& data) +{ + add (data.first, data.second); +} + +CSMDoc::Messages::Iterator CSMDoc::Messages::begin() const +{ + return mMessages.begin(); +} + +CSMDoc::Messages::Iterator CSMDoc::Messages::end() const +{ + return mMessages.end(); +} \ No newline at end of file diff --git a/apps/opencs/model/doc/messages.hpp b/apps/opencs/model/doc/messages.hpp new file mode 100644 index 000000000..0f36c73a7 --- /dev/null +++ b/apps/opencs/model/doc/messages.hpp @@ -0,0 +1,44 @@ +#ifndef CSM_DOC_MESSAGES_H +#define CSM_DOC_MESSAGES_H + +#include +#include + +#include "../world/universalid.hpp" + +namespace CSMDoc +{ + class Messages + { + public: + + struct Message + { + CSMWorld::UniversalId mId; + std::string mMessage; + std::string mHint; + }; + + typedef std::vector Collection; + + typedef Collection::const_iterator Iterator; + + private: + + Collection mMessages; + + public: + + void add (const CSMWorld::UniversalId& id, const std::string& message, + const std::string& hint = ""); + + /// \deprecated Use add instead. + void push_back (const std::pair& data); + + Iterator begin() const; + + Iterator end() const; + }; +} + +#endif diff --git a/apps/opencs/model/doc/operation.cpp b/apps/opencs/model/doc/operation.cpp index 42a432043..e728050f4 100644 --- a/apps/opencs/model/doc/operation.cpp +++ b/apps/opencs/model/doc/operation.cpp @@ -27,7 +27,9 @@ void CSMDoc::Operation::prepareStages() } CSMDoc::Operation::Operation (int type, bool ordered, bool finalAlways) -: mType (type), mOrdered (ordered), mFinalAlways (finalAlways) +: mType (type), mStages(std::vector >()), mCurrentStage(mStages.begin()), + mCurrentStep(0), mCurrentStepTotal(0), mTotalSteps(0), mOrdered (ordered), + mFinalAlways (finalAlways), mError(false) { connect (this, SIGNAL (finished()), this, SLOT (operationDone())); } @@ -82,7 +84,7 @@ void CSMDoc::Operation::abort() void CSMDoc::Operation::executeStage() { - Stage::Messages messages; + Messages messages; while (mCurrentStage!=mStages.end()) { @@ -99,7 +101,7 @@ void CSMDoc::Operation::executeStage() } catch (const std::exception& e) { - emit reportMessage (CSMWorld::UniversalId(), e.what(), mType); + emit reportMessage (CSMWorld::UniversalId(), e.what(), "", mType); abort(); } @@ -110,8 +112,8 @@ void CSMDoc::Operation::executeStage() emit progress (mCurrentStepTotal, mTotalSteps ? mTotalSteps : 1, mType); - for (Stage::Messages::const_iterator iter (messages.begin()); iter!=messages.end(); ++iter) - emit reportMessage (iter->first, iter->second, mType); + for (Messages::Iterator iter (messages.begin()); iter!=messages.end(); ++iter) + emit reportMessage (iter->mId, iter->mMessage, iter->mHint, mType); if (mCurrentStage==mStages.end()) exit(); @@ -119,5 +121,5 @@ void CSMDoc::Operation::executeStage() void CSMDoc::Operation::operationDone() { - emit done (mType); -} \ No newline at end of file + emit done (mType, mError); +} diff --git a/apps/opencs/model/doc/operation.hpp b/apps/opencs/model/doc/operation.hpp index 651283880..3c9467754 100644 --- a/apps/opencs/model/doc/operation.hpp +++ b/apps/opencs/model/doc/operation.hpp @@ -52,9 +52,9 @@ namespace CSMDoc void progress (int current, int max, int type); void reportMessage (const CSMWorld::UniversalId& id, const std::string& message, - int type); + const std::string& hint, int type); - void done (int type); + void done (int type, bool failed); public slots: diff --git a/apps/opencs/model/doc/runner.cpp b/apps/opencs/model/doc/runner.cpp new file mode 100644 index 000000000..d679c1890 --- /dev/null +++ b/apps/opencs/model/doc/runner.cpp @@ -0,0 +1,162 @@ + +#include "runner.hpp" + +#include +#include +#include +#include + +#include "operation.hpp" + +CSMDoc::Runner::Runner (const boost::filesystem::path& projectPath) +: mRunning (false), mStartup (0), mProjectPath (projectPath) +{ + connect (&mProcess, SIGNAL (finished (int, QProcess::ExitStatus)), + this, SLOT (finished (int, QProcess::ExitStatus))); + + connect (&mProcess, SIGNAL (readyReadStandardOutput()), + this, SLOT (readyReadStandardOutput())); + + mProcess.setProcessChannelMode (QProcess::MergedChannels); + + mProfile.blank(); +} + +CSMDoc::Runner::~Runner() +{ + if (mRunning) + { + disconnect (&mProcess, 0, this, 0); + mProcess.kill(); + mProcess.waitForFinished(); + } +} + +void CSMDoc::Runner::start (bool delayed) +{ + if (mStartup) + { + delete mStartup; + mStartup = 0; + } + + if (!delayed) + { + mLog.clear(); + + QString path = "openmw"; +#ifdef Q_OS_WIN + path.append(QString(".exe")); +#elif defined(Q_OS_MAC) + QDir dir(QCoreApplication::applicationDirPath()); + dir.cdUp(); + dir.cdUp(); + dir.cdUp(); + path = dir.absoluteFilePath(path.prepend("OpenMW.app/Contents/MacOS/")); +#else + path.prepend(QString("./")); +#endif + + mStartup = new QTemporaryFile (this); + mStartup->open(); + + { + QTextStream stream (mStartup); + + if (!mStartupInstruction.empty()) + stream << QString::fromUtf8 (mStartupInstruction.c_str()) << '\n'; + + stream << QString::fromUtf8 (mProfile.mScriptText.c_str()); + } + + mStartup->close(); + + QStringList arguments; + arguments << "--skip-menu"; + + if (mProfile.mFlags & ESM::DebugProfile::Flag_BypassNewGame) + arguments << "--new-game=0"; + else + arguments << "--new-game=1"; + + arguments << ("--script-run="+mStartup->fileName());; + + arguments << + QString::fromUtf8 (("--data="+mProjectPath.parent_path().string()).c_str()); + + for (std::vector::const_iterator iter (mContentFiles.begin()); + iter!=mContentFiles.end(); ++iter) + { + arguments << QString::fromUtf8 (("--content="+*iter).c_str()); + } + + arguments + << QString::fromUtf8 (("--content="+mProjectPath.filename().string()).c_str()); + + mProcess.start (path, arguments); + } + + mRunning = true; + emit runStateChanged(); +} + +void CSMDoc::Runner::stop() +{ + delete mStartup; + mStartup = 0; + + if (mProcess.state()==QProcess::NotRunning) + { + mRunning = false; + emit runStateChanged(); + } + else + mProcess.kill(); +} + +bool CSMDoc::Runner::isRunning() const +{ + return mRunning; +} + +void CSMDoc::Runner::configure (const ESM::DebugProfile& profile, + const std::vector& contentFiles, const std::string& startupInstruction) +{ + mProfile = profile; + mContentFiles = contentFiles; + mStartupInstruction = startupInstruction; +} + +void CSMDoc::Runner::finished (int exitCode, QProcess::ExitStatus exitStatus) +{ + mRunning = false; + emit runStateChanged(); +} + +QTextDocument *CSMDoc::Runner::getLog() +{ + return &mLog; +} + +void CSMDoc::Runner::readyReadStandardOutput() +{ + mLog.setPlainText ( + mLog.toPlainText() + QString::fromUtf8 (mProcess.readAllStandardOutput())); +} + + +CSMDoc::SaveWatcher::SaveWatcher (Runner *runner, Operation *operation) +: QObject (runner), mRunner (runner) +{ + connect (operation, SIGNAL (done (int, bool)), this, SLOT (saveDone (int, bool))); +} + +void CSMDoc::SaveWatcher::saveDone (int type, bool failed) +{ + if (failed) + mRunner->stop(); + else + mRunner->start(); + + deleteLater(); +} diff --git a/apps/opencs/model/doc/runner.hpp b/apps/opencs/model/doc/runner.hpp new file mode 100644 index 000000000..38b52a73b --- /dev/null +++ b/apps/opencs/model/doc/runner.hpp @@ -0,0 +1,85 @@ +#ifndef CSM_DOC_RUNNER_H +#define CSM_DOC_RUNNER_H + +#include +#include + +#include + +#include +#include +#include + +#include + +class QTemporaryFile; + +namespace CSMDoc +{ + class Runner : public QObject + { + Q_OBJECT + + QProcess mProcess; + bool mRunning; + ESM::DebugProfile mProfile; + std::vector mContentFiles; + std::string mStartupInstruction; + QTemporaryFile *mStartup; + QTextDocument mLog; + boost::filesystem::path mProjectPath; + + public: + + Runner (const boost::filesystem::path& projectPath); + + ~Runner(); + + /// \param delayed Flag as running but do not start the OpenMW process yet (the + /// process must be started by another call of start with delayed==false) + void start (bool delayed = false); + + void stop(); + + /// \note Running state is entered when the start function is called. This + /// is not necessarily identical to the moment the child process is started. + bool isRunning() const; + + void configure (const ESM::DebugProfile& profile, + const std::vector& contentFiles, + const std::string& startupInstruction); + + QTextDocument *getLog(); + + signals: + + void runStateChanged(); + + private slots: + + void finished (int exitCode, QProcess::ExitStatus exitStatus); + + void readyReadStandardOutput(); + }; + + class Operation; + + /// \brief Watch for end of save operation and restart or stop runner + class SaveWatcher : public QObject + { + Q_OBJECT + + Runner *mRunner; + + public: + + /// *this attaches itself to runner + SaveWatcher (Runner *runner, Operation *operation); + + private slots: + + void saveDone (int type, bool failed); + }; +} + +#endif diff --git a/apps/opencs/model/doc/saving.cpp b/apps/opencs/model/doc/saving.cpp index 95631eea9..b52186a47 100644 --- a/apps/opencs/model/doc/saving.cpp +++ b/apps/opencs/model/doc/saving.cpp @@ -17,7 +17,14 @@ CSMDoc::Saving::Saving (Document& document, const boost::filesystem::path& proje appendStage (new WriteHeaderStage (mDocument, mState, true)); - appendStage (new WriteFilterStage (mDocument, mState, CSMFilter::Filter::Scope_Project)); + appendStage (new WriteCollectionStage > ( + mDocument.getData().getFilters(), mState, CSMWorld::Scope_Project)); + + appendStage (new WriteCollectionStage > ( + mDocument.getData().getDebugProfiles(), mState, CSMWorld::Scope_Project)); + + appendStage (new WriteCollectionStage > ( + mDocument.getData().getScripts(), mState, CSMWorld::Scope_Project)); appendStage (new CloseSaveStage (mState)); @@ -65,6 +72,15 @@ CSMDoc::Saving::Saving (Document& document, const boost::filesystem::path& proje appendStage (new WriteCollectionStage > (mDocument.getData().getBodyParts(), mState)); + appendStage (new WriteCollectionStage > + (mDocument.getData().getSoundGens(), mState)); + + appendStage (new WriteCollectionStage > + (mDocument.getData().getMagicEffects(), mState)); + + appendStage (new WriteCollectionStage > + (mDocument.getData().getStartScripts(), mState)); + appendStage (new WriteDialogueCollectionStage (mDocument, mState, false)); appendStage (new WriteDialogueCollectionStage (mDocument, mState, true)); @@ -75,6 +91,8 @@ CSMDoc::Saving::Saving (Document& document, const boost::filesystem::path& proje appendStage (new WriteCellCollectionStage (mDocument, mState)); + appendStage (new WritePathgridCollectionStage (mDocument, mState)); + // close file and clean up appendStage (new CloseSaveStage (mState)); diff --git a/apps/opencs/model/doc/savingstages.cpp b/apps/opencs/model/doc/savingstages.cpp index d36c2fb86..08f8c9eaa 100644 --- a/apps/opencs/model/doc/savingstages.cpp +++ b/apps/opencs/model/doc/savingstages.cpp @@ -201,23 +201,6 @@ void CSMDoc::WriteRefIdCollectionStage::perform (int stage, Messages& messages) } -CSMDoc::WriteFilterStage::WriteFilterStage (Document& document, SavingState& state, - CSMFilter::Filter::Scope scope) -: WriteCollectionStage > (document.getData().getFilters(), - state), - mDocument (document), mScope (scope) -{} - -void CSMDoc::WriteFilterStage::perform (int stage, Messages& messages) -{ - const CSMWorld::Record& record = - mDocument.getData().getFilters().getRecord (stage); - - if (record.get().mScope==mScope) - WriteCollectionStage >::perform (stage, messages); -} - - CSMDoc::CollectionReferencesStage::CollectionReferencesStage (Document& document, SavingState& state) : mDocument (document), mState (state) @@ -301,20 +284,6 @@ void CSMDoc::WriteCellCollectionStage::perform (int stage, Messages& messages) // write references if (references!=mState.getSubRecords().end()) { - // first pass: find highest RefNum - int lastRefNum = -1; - - for (std::vector::const_iterator iter (references->second.begin()); - iter!=references->second.end(); ++iter) - { - const CSMWorld::Record& ref = - mDocument.getData().getReferences().getRecord (*iter); - - if (ref.get().mRefNum.mContentFile==0 && ref.get().mRefNum.mIndex>lastRefNum) - lastRefNum = ref.get().mRefNum.mIndex; - } - - // second pass: write for (std::vector::const_iterator iter (references->second.begin()); iter!=references->second.end(); ++iter) { @@ -324,20 +293,7 @@ void CSMDoc::WriteCellCollectionStage::perform (int stage, Messages& messages) if (ref.mState==CSMWorld::RecordBase::State_Modified || ref.mState==CSMWorld::RecordBase::State_ModifiedOnly) { - if (ref.get().mRefNum.mContentFile==-2) - { - if (lastRefNum>=0xffffff) - throw std::runtime_error ( - "RefNums exhausted in cell: " + cell.get().mId); - - ESM::CellRef ref2 = ref.get(); - ref2.mRefNum.mContentFile = 0; - ref2.mRefNum.mIndex = ++lastRefNum; - - ref2.save (mState.getWriter()); - } - else - ref.get().save (mState.getWriter()); + ref.get().save (mState.getWriter()); } else if (ref.mState==CSMWorld::RecordBase::State_Deleted) { @@ -355,6 +311,48 @@ void CSMDoc::WriteCellCollectionStage::perform (int stage, Messages& messages) } +CSMDoc::WritePathgridCollectionStage::WritePathgridCollectionStage (Document& document, + SavingState& state) +: mDocument (document), mState (state) +{} + +int CSMDoc::WritePathgridCollectionStage::setup() +{ + return mDocument.getData().getPathgrids().getSize(); +} + +void CSMDoc::WritePathgridCollectionStage::perform (int stage, Messages& messages) +{ + const CSMWorld::Record& pathgrid = + mDocument.getData().getPathgrids().getRecord (stage); + + if (pathgrid.mState==CSMWorld::RecordBase::State_Modified || + pathgrid.mState==CSMWorld::RecordBase::State_ModifiedOnly) + { + CSMWorld::Pathgrid record = pathgrid.get(); + + if (record.mId.substr (0, 1)=="#") + { + std::istringstream stream (record.mId.c_str()); + char ignore; + stream >> ignore >> record.mData.mX >> record.mData.mY; + } + else + record.mCell = record.mId; + + mState.getWriter().startRecord (record.sRecordId); + + record.save (mState.getWriter()); + + mState.getWriter().endRecord (record.sRecordId); + } + else if (pathgrid.mState==CSMWorld::RecordBase::State_Deleted) + { + /// \todo write record with delete flag + } +} + + CSMDoc::CloseSaveStage::CloseSaveStage (SavingState& state) : mState (state) {} diff --git a/apps/opencs/model/doc/savingstages.hpp b/apps/opencs/model/doc/savingstages.hpp index dcb1a8650..907041114 100644 --- a/apps/opencs/model/doc/savingstages.hpp +++ b/apps/opencs/model/doc/savingstages.hpp @@ -5,8 +5,9 @@ #include "../world/record.hpp" #include "../world/idcollection.hpp" +#include "../world/scope.hpp" -#include "../filter/filter.hpp" +#include #include "savingstate.hpp" @@ -67,10 +68,12 @@ namespace CSMDoc { const CollectionT& mCollection; SavingState& mState; + CSMWorld::Scope mScope; public: - WriteCollectionStage (const CollectionT& collection, SavingState& state); + WriteCollectionStage (const CollectionT& collection, SavingState& state, + CSMWorld::Scope scope = CSMWorld::Scope_Content); virtual int setup(); ///< \return number of steps @@ -81,8 +84,8 @@ namespace CSMDoc template WriteCollectionStage::WriteCollectionStage (const CollectionT& collection, - SavingState& state) - : mCollection (collection), mState (state) + SavingState& state, CSMWorld::Scope scope) + : mCollection (collection), mState (state), mScope (scope) {} template @@ -94,18 +97,23 @@ namespace CSMDoc template void WriteCollectionStage::perform (int stage, Messages& messages) { + if (CSMWorld::getScopeFromId (mCollection.getRecord (stage).get().mId)!=mScope) + return; + CSMWorld::RecordBase::State state = mCollection.getRecord (stage).mState; if (state==CSMWorld::RecordBase::State_Modified || state==CSMWorld::RecordBase::State_ModifiedOnly) { - std::string type; - for (int i=0; i<4; ++i) - /// \todo make endianess agnostic (change ESMWriter interface?) - type += reinterpret_cast (&mCollection.getRecord (stage).mModified.sRecordId)[i]; - - mState.getWriter().startRecord (mCollection.getRecord (stage).mModified.sRecordId); - mState.getWriter().writeHNCString ("NAME", mCollection.getId (stage)); + // FIXME: A quick Workaround to support records which should not write + // NAME, including SKIL, MGEF and SCPT. If there are many more + // idcollection records that doesn't use NAME then a more generic + // solution may be required. + uint32_t name = mCollection.getRecord (stage).mModified.sRecordId; + mState.getWriter().startRecord (name); + + if(name != ESM::REC_SKIL && name != ESM::REC_MGEF && name != ESM::REC_SCPT) + mState.getWriter().writeHNCString ("NAME", mCollection.getId (stage)); mCollection.getRecord (stage).mModified.save (mState.getWriter()); mState.getWriter().endRecord (mCollection.getRecord (stage).mModified.sRecordId); } @@ -152,28 +160,30 @@ namespace CSMDoc }; - class WriteFilterStage : public WriteCollectionStage > + class CollectionReferencesStage : public Stage { Document& mDocument; - CSMFilter::Filter::Scope mScope; + SavingState& mState; public: - WriteFilterStage (Document& document, SavingState& state, CSMFilter::Filter::Scope scope); + CollectionReferencesStage (Document& document, SavingState& state); + + virtual int setup(); + ///< \return number of steps virtual void perform (int stage, Messages& messages); ///< Messages resulting from this stage will be appended to \a messages. - }; - class CollectionReferencesStage : public Stage + class WriteCellCollectionStage : public Stage { Document& mDocument; SavingState& mState; public: - CollectionReferencesStage (Document& document, SavingState& state); + WriteCellCollectionStage (Document& document, SavingState& state); virtual int setup(); ///< \return number of steps @@ -182,14 +192,15 @@ namespace CSMDoc ///< Messages resulting from this stage will be appended to \a messages. }; - class WriteCellCollectionStage : public Stage + + class WritePathgridCollectionStage : public Stage { Document& mDocument; SavingState& mState; public: - WriteCellCollectionStage (Document& document, SavingState& state); + WritePathgridCollectionStage (Document& document, SavingState& state); virtual int setup(); ///< \return number of steps diff --git a/apps/opencs/model/doc/stage.hpp b/apps/opencs/model/doc/stage.hpp index ca34c2229..126823ae9 100644 --- a/apps/opencs/model/doc/stage.hpp +++ b/apps/opencs/model/doc/stage.hpp @@ -6,14 +6,14 @@ #include "../world/universalid.hpp" +#include "messages.hpp" + namespace CSMDoc { class Stage { public: - typedef std::vector > Messages; - virtual ~Stage(); virtual int setup() = 0; diff --git a/apps/opencs/model/doc/state.hpp b/apps/opencs/model/doc/state.hpp index 6e1a1c4f4..287439a8b 100644 --- a/apps/opencs/model/doc/state.hpp +++ b/apps/opencs/model/doc/state.hpp @@ -8,12 +8,13 @@ namespace CSMDoc State_Modified = 1, State_Locked = 2, State_Operation = 4, + State_Running = 8, - State_Saving = 8, - State_Verifying = 16, - State_Compiling = 32, // not implemented yet - State_Searching = 64, // not implemented yet - State_Loading = 128 // pseudo-state; can not be encountered in a loaded document + State_Saving = 16, + State_Verifying = 32, + State_Compiling = 64, // not implemented yet + State_Searching = 128, // not implemented yet + State_Loading = 256 // pseudo-state; can not be encountered in a loaded document }; } diff --git a/apps/opencs/model/filter/filter.hpp b/apps/opencs/model/filter/filter.hpp deleted file mode 100644 index 62170ca80..000000000 --- a/apps/opencs/model/filter/filter.hpp +++ /dev/null @@ -1,25 +0,0 @@ -#ifndef CSM_FILTER_FILTER_H -#define CSM_FILTER_FILTER_H - -#include -#include - -#include - -namespace CSMFilter -{ - /// \brief Wrapper for Filter record - struct Filter : public ESM::Filter - { - enum Scope - { - Scope_Project = 0, // per project - Scope_Session = 1, // exists only for one editing session; not saved - Scope_Content = 2 // embedded in the edited content file - }; - - Scope mScope; - }; -} - -#endif \ No newline at end of file diff --git a/apps/opencs/model/filter/parser.cpp b/apps/opencs/model/filter/parser.cpp index bec445cbc..51338dfc9 100644 --- a/apps/opencs/model/filter/parser.cpp +++ b/apps/opencs/model/filter/parser.cpp @@ -596,7 +596,7 @@ bool CSMFilter::Parser::parse (const std::string& filter, bool allowPredefined) return false; } - const CSMWorld::Record& record = mData.getFilters().getRecord (index); + const CSMWorld::Record& record = mData.getFilters().getRecord (index); if (record.isDeleted()) { diff --git a/apps/opencs/model/filter/valuenode.cpp b/apps/opencs/model/filter/valuenode.cpp index 66b6282d7..26b982441 100644 --- a/apps/opencs/model/filter/valuenode.cpp +++ b/apps/opencs/model/filter/valuenode.cpp @@ -27,7 +27,7 @@ bool CSMFilter::ValueNode::test (const CSMWorld::IdTableBase& table, int row, QVariant data = table.data (index); if (data.type()!=QVariant::Double && data.type()!=QVariant::Bool && data.type()!=QVariant::Int && - data.type()!=QVariant::UInt && data.type()!=static_cast (QMetaType::Float)) + data.type()!=QVariant::UInt) return false; double value = data.toDouble(); diff --git a/apps/opencs/model/settings/setting.cpp b/apps/opencs/model/settings/setting.cpp index 2f86d4ff8..9e33ab916 100644 --- a/apps/opencs/model/settings/setting.cpp +++ b/apps/opencs/model/settings/setting.cpp @@ -2,8 +2,8 @@ #include "support.hpp" CSMSettings::Setting::Setting(SettingType typ, const QString &settingName, - const QString &pageName) - : mIsEditorSetting (false) + const QString &pageName, const QString& label) +: mIsEditorSetting (true) { buildDefaultSetting(); @@ -17,6 +17,7 @@ CSMSettings::Setting::Setting(SettingType typ, const QString &settingName, setProperty (Property_SettingType, QVariant (settingType).toString()); setProperty (Property_Page, pageName); setProperty (Property_Name, settingName); + setProperty (Property_Label, label.isEmpty() ? settingName : label); } void CSMSettings::Setting::buildDefaultSetting() @@ -194,6 +195,16 @@ QString CSMSettings::Setting::page() const return property (Property_Page).at(0); } +void CSMSettings::Setting::setStyleSheet (const QString &value) +{ + setProperty (Property_StyleSheet, value); +} + +QString CSMSettings::Setting::styleSheet() const +{ + return property (Property_StyleSheet).at(0); +} + void CSMSettings::Setting::setPrefix (const QString &value) { setProperty (Property_Prefix, value); @@ -280,14 +291,16 @@ CSMSettings::SettingType CSMSettings::Setting::type() const Property_SettingType).at(0).toInt()); } -void CSMSettings::Setting::setMaximum (int value) +void CSMSettings::Setting::setRange (int min, int max) { - setProperty (Property_Maximum, value); + setProperty (Property_Minimum, min); + setProperty (Property_Maximum, max); } -void CSMSettings::Setting::setMaximum (double value) +void CSMSettings::Setting::setRange (double min, double max) { - setProperty (Property_Maximum, value); + setProperty (Property_Minimum, min); + setProperty (Property_Maximum, max); } QString CSMSettings::Setting::maximum() const @@ -295,16 +308,6 @@ QString CSMSettings::Setting::maximum() const return property (Property_Maximum).at(0); } -void CSMSettings::Setting::setMinimum (int value) -{ - setProperty (Property_Minimum, value); -} - -void CSMSettings::Setting::setMinimum (double value) -{ - setProperty (Property_Minimum, value); -} - QString CSMSettings::Setting::minimum() const { return property (Property_Minimum).at(0); @@ -362,6 +365,26 @@ bool CSMSettings::Setting::wrapping() const return (property (Property_Wrapping).at(0) == "true"); } +void CSMSettings::Setting::setLabel (const QString& label) +{ + setProperty (Property_Label, label); +} + +QString CSMSettings::Setting::getLabel() const +{ + return property (Property_Label).at (0); +} + +void CSMSettings::Setting::setToolTip (const QString& toolTip) +{ + setProperty (Property_ToolTip, toolTip); +} + +QString CSMSettings::Setting::getToolTip() const +{ + return property (Property_ToolTip).at (0); +} + void CSMSettings::Setting::setProperty (SettingProperty prop, bool value) { setProperty (prop, QStringList() << QVariant (value).toString()); diff --git a/apps/opencs/model/settings/setting.hpp b/apps/opencs/model/settings/setting.hpp index e40302f00..be51a531a 100644 --- a/apps/opencs/model/settings/setting.hpp +++ b/apps/opencs/model/settings/setting.hpp @@ -29,8 +29,8 @@ namespace CSMSettings public: - explicit Setting(SettingType typ, const QString &settingName, - const QString &pageName); + Setting(SettingType typ, const QString &settingName, + const QString &pageName, const QString& label = ""); void addProxy (const Setting *setting, const QStringList &vals); void addProxy (const Setting *setting, const QList &list); @@ -66,12 +66,11 @@ namespace CSMSettings void setMask (const QString &value); QString mask() const; - void setMaximum (int value); - void setMaximum (double value); + void setRange (int min, int max); + void setRange (double min, double max); + QString maximum() const; - void setMinimum (int value); - void setMinimum (double value); QString minimum() const; void setName (const QString &value); @@ -80,6 +79,9 @@ namespace CSMSettings void setPage (const QString &value); QString page() const; + void setStyleSheet (const QString &value); + QString styleSheet() const; + void setPrefix (const QString &value); QString prefix() const; @@ -129,6 +131,13 @@ namespace CSMSettings void setWidgetWidth (int value); int widgetWidth() const; + /// This is the text the user gets to see. + void setLabel (const QString& label); + QString getLabel() const; + + void setToolTip (const QString& toolTip); + QString getToolTip() const; + ///returns the specified property value QStringList property (SettingProperty prop) const; diff --git a/apps/opencs/model/settings/support.hpp b/apps/opencs/model/settings/support.hpp index 229e293b8..ab0e5088c 100644 --- a/apps/opencs/model/settings/support.hpp +++ b/apps/opencs/model/settings/support.hpp @@ -35,12 +35,15 @@ namespace CSMSettings Property_TickInterval = 19, Property_TicksAbove = 20, Property_TicksBelow = 21, + Property_StyleSheet = 22, + Property_Label = 23, + Property_ToolTip = 24, //Stringlists should always be the last items - Property_DefaultValues = 22, - Property_DeclaredValues = 23, - Property_DefinedValues = 24, - Property_Proxies = 25 + Property_DefaultValues = 25, + Property_DeclaredValues = 26, + Property_DefinedValues = 27, + Property_Proxies = 28 }; ///Basic setting widget types. @@ -106,7 +109,7 @@ namespace CSMSettings "is_multi_line", "widget_width", "view_row", "view_column", "delimiter", "is_serializable","column_span", "row_span", "minimum", "maximum", "special_value_text", "prefix", "suffix", "single_step", "wrapping", - "tick_interval", "ticks_above", "ticks_below", + "tick_interval", "ticks_above", "ticks_below", "stylesheet", "defaults", "declarations", "definitions", "proxies" }; @@ -135,6 +138,7 @@ namespace CSMSettings "1", //tick interval "false", //ticks above "true", //ticks below + "", //StyleSheet "", //default values "", //declared values "", //defined values diff --git a/apps/opencs/model/settings/usersettings.cpp b/apps/opencs/model/settings/usersettings.cpp index 04f98f0d6..7dac660c3 100644 --- a/apps/opencs/model/settings/usersettings.cpp +++ b/apps/opencs/model/settings/usersettings.cpp @@ -4,12 +4,16 @@ #include #include +#include #include #include "setting.hpp" #include "support.hpp" +#include #include +#include + /** * Workaround for problems with whitespaces in paths in older versions of Boost library */ @@ -26,81 +30,184 @@ namespace boost } /* namespace boost */ #endif /* (BOOST_VERSION <= 104600) */ -CSMSettings::UserSettings *CSMSettings::UserSettings::mUserSettingsInstance = 0; +CSMSettings::UserSettings *CSMSettings::UserSettings::sUserSettingsInstance = 0; -CSMSettings::UserSettings::UserSettings (const Files::ConfigurationManager& configurationManager) -: mCfgMgr (configurationManager) + CSMSettings::UserSettings::UserSettings (const Files::ConfigurationManager& configurationManager) + : mCfgMgr (configurationManager) + , mSettingDefinitions(NULL) { - assert(!mUserSettingsInstance); - mUserSettingsInstance = this; - - mSettingDefinitions = 0; + assert(!sUserSettingsInstance); + sUserSettingsInstance = this; buildSettingModelDefaults(); } void CSMSettings::UserSettings::buildSettingModelDefaults() { - QString section = "Window Size"; - { - Setting *width = createSetting (Type_LineEdit, section, "Width"); - Setting *height = createSetting (Type_LineEdit, section, "Height"); - - width->setWidgetWidth (5); - height->setWidgetWidth (8); - - width->setDefaultValues (QStringList() << "1024"); - height->setDefaultValues (QStringList() << "768"); - - width->setEditorSetting (true); - height->setEditorSetting (true); + QString section; - height->setViewLocation (2,2); - width->setViewLocation (2,1); + declareSection ("3d-render", "3D Rendering"); + { + Setting *shaders = createSetting (Type_CheckBox, "shaders", "Enable Shaders"); + shaders->setDefaultValue ("true"); + + Setting *farClipDist = createSetting (Type_DoubleSpinBox, "far-clip-distance", "Far clipping distance"); + farClipDist->setDefaultValue (300000); + farClipDist->setRange (0, 1000000); + farClipDist->setToolTip ("The maximum distance objects are still rendered at."); + + QString defaultValue = "None"; + Setting *antialiasing = createSetting (Type_ComboBox, "antialiasing", "Antialiasing"); + antialiasing->setDeclaredValues (QStringList() + << defaultValue << "MSAA 2" << "MSAA 4" << "MSAA 8" << "MSAA 16"); + antialiasing->setDefaultValue (defaultValue); + } - /* - *Create the proxy setting for predefined values - */ - Setting *preDefined = createSetting (Type_ComboBox, section, - "Pre-Defined"); + declareSection ("3d-render-adv", "3D Rendering (Advanced)"); + { + Setting *numLights = createSetting (Type_SpinBox, "num_lights", + "Number of lights per pass"); + numLights->setDefaultValue (8); + numLights->setRange (1, 100); + } - preDefined->setDeclaredValues (QStringList() << "640 x 480" - << "800 x 600" << "1024 x 768" << "1440 x 900"); + declareSection ("scene-input", "Scene Input"); + { + Setting *timer = createSetting (Type_SpinBox, "timer", "Input responsiveness"); + timer->setDefaultValue (20); + timer->setRange (1, 100); + timer->setToolTip ("The time between two checks for user input in milliseconds.

" + "Lower value result in higher responsiveness."); + + Setting *fastFactor = createSetting (Type_SpinBox, "fast-factor", + "Fast movement factor"); + fastFactor->setDefaultValue (4); + fastFactor->setRange (1, 100); + fastFactor->setToolTip ( + "Factor by which movement is speed up while the shift key is held down."); + } + declareSection ("window", "Window"); + { + Setting *preDefined = createSetting (Type_ComboBox, "pre-defined", + "Default window size"); + preDefined->setEditorSetting (false); + preDefined->setDeclaredValues ( + QStringList() << "640 x 480" << "800 x 600" << "1024 x 768" << "1440 x 900"); preDefined->setViewLocation (1, 1); - preDefined->setWidgetWidth (10); preDefined->setColumnSpan (2); + preDefined->setToolTip ("Newly opened top-level windows will open with this size " + "(picked from a list of pre-defined values)"); - preDefined->addProxy (width, - QStringList() << "640" << "800" << "1024" << "1440" - ); + Setting *width = createSetting (Type_LineEdit, "default-width", + "Default window width"); + width->setDefaultValues (QStringList() << "1024"); + width->setViewLocation (2, 1); + width->setColumnSpan (1); + width->setToolTip ("Newly opened top-level windows will open with this width."); + preDefined->addProxy (width, QStringList() << "640" << "800" << "1024" << "1440"); - preDefined->addProxy (height, - QStringList() << "480" << "600" << "768" << "900" - ); + Setting *height = createSetting (Type_LineEdit, "default-height", + "Default window height"); + height->setDefaultValues (QStringList() << "768"); + height->setViewLocation (2, 2); + height->setColumnSpan (1); + height->setToolTip ("Newly opened top-level windows will open with this height."); + preDefined->addProxy (height, QStringList() << "480" << "600" << "768" << "900"); + + Setting *reuse = createSetting (Type_CheckBox, "reuse", "Reuse Subviews"); + reuse->setDefaultValue ("true"); + reuse->setToolTip ("When a new subview is requested and a matching subview already " + " exist, do not open a new subview and use the existing one instead."); + + Setting *statusBar = createSetting (Type_CheckBox, "show-statusbar", "Show Status Bar"); + statusBar->setDefaultValue ("true"); + statusBar->setToolTip ("If a newly open top level window is showing status bars or not. " + " Note that this does not affect existing windows."); + + Setting *maxSubView = createSetting (Type_SpinBox, "max-subviews", + "Maximum number of subviews per top-level window"); + maxSubView->setDefaultValue (256); + maxSubView->setRange (1, 256); + maxSubView->setToolTip ("If the maximum number is reached and a new subview is opened " + "it will be placed into a new top-level window."); + + Setting *hide = createSetting (Type_CheckBox, "hide-subview", "Hide single subview"); + hide->setDefaultValue ("false"); + hide->setToolTip ("When a view contains only a single subview, hide the subview title " + "bar and if this subview is closed also close the view (unless it is the last " + "view for this document)"); + + Setting *minWidth = createSetting (Type_SpinBox, "minimum-width", + "Minimum subview width"); + minWidth->setDefaultValue (325); + minWidth->setRange (50, 10000); + minWidth->setToolTip ("Minimum width of subviews."); } - section = "Display Format"; + declareSection ("records", "Records"); { QString defaultValue = "Icon and Text"; + QStringList values = QStringList() << defaultValue << "Icon Only" << "Text Only"; - QStringList values = QStringList() - << defaultValue << "Icon Only" << "Text Only"; - - Setting *rsd = createSetting (Type_RadioButton, - section, "Record Status Display"); - - Setting *ritd = createSetting (Type_RadioButton, - section, "Referenceable ID Type Display"); - + Setting *rsd = createSetting (Type_RadioButton, "status-format", + "Modification status display format"); + rsd->setDefaultValue (defaultValue); rsd->setDeclaredValues (values); + + Setting *ritd = createSetting (Type_RadioButton, "type-format", + "ID type display format"); + ritd->setDefaultValue (defaultValue); ritd->setDeclaredValues (values); + } - rsd->setEditorSetting (true); - ritd->setEditorSetting (true); + declareSection ("table-input", "Table Input"); + { + QString inPlaceEdit ("Edit in Place"); + QString editRecord ("Edit Record"); + QString view ("View"); + QString editRecordAndClose ("Edit Record and Close"); + + QStringList values; + values + << "None" << inPlaceEdit << editRecord << view << "Revert" << "Delete" + << editRecordAndClose << "View and Close"; + + QString toolTip = "

    " + "
  • None
  • " + "
  • Edit in Place: Edit the clicked cell
  • " + "
  • Edit Record: Open a dialogue subview for the clicked record
  • " + "
  • View: Open a scene subview for the clicked record (not available everywhere)
  • " + "
  • Revert: Revert record
  • " + "
  • Delete: Delete recordy
  • " + "
  • Edit Record and Close: Open a dialogue subview for the clicked record and close the table subview
  • " + "
  • View And Close: Open a scene subview for the clicked record and close the table subview
  • " + "
"; + + Setting *doubleClick = createSetting (Type_ComboBox, "double", "Double Click"); + doubleClick->setDeclaredValues (values); + doubleClick->setDefaultValue (inPlaceEdit); + doubleClick->setToolTip ("Action on double click in table:

" + toolTip); + + Setting *shiftDoubleClick = createSetting (Type_ComboBox, "double-s", + "Shift Double Click"); + shiftDoubleClick->setDeclaredValues (values); + shiftDoubleClick->setDefaultValue (editRecord); + shiftDoubleClick->setToolTip ("Action on shift double click in table:

" + toolTip); + + Setting *ctrlDoubleClick = createSetting (Type_ComboBox, "double-c", + "Control Double Click"); + ctrlDoubleClick->setDeclaredValues (values); + ctrlDoubleClick->setDefaultValue (view); + ctrlDoubleClick->setToolTip ("Action on control double click in table:

" + toolTip); + + Setting *shiftCtrlDoubleClick = createSetting (Type_ComboBox, "double-sc", + "Shift Control Double Click"); + shiftCtrlDoubleClick->setDeclaredValues (values); + shiftCtrlDoubleClick->setDefaultValue (editRecordAndClose); + shiftCtrlDoubleClick->setToolTip ("Action on shift control double click in table:

" + toolTip); } - section = "Proxy Selection Test"; { /****************************************************************** * There are three types of values: @@ -276,7 +383,7 @@ void CSMSettings::UserSettings::buildSettingModelDefaults() CSMSettings::UserSettings::~UserSettings() { - mUserSettingsInstance = 0; + sUserSettingsInstance = 0; } void CSMSettings::UserSettings::loadSettings (const QString &fileName) @@ -307,8 +414,21 @@ void CSMSettings::UserSettings::loadSettings (const QString &fileName) (QSettings::IniFormat, QSettings::UserScope, "opencs", QString(), this); } -bool CSMSettings::UserSettings::hasSettingDefinitions - (const QString &viewKey) const +// if the key is not found create one with a default value +QString CSMSettings::UserSettings::setting(const QString &viewKey, const QString &value) +{ + if(mSettingDefinitions->contains(viewKey)) + return settingValue(viewKey); + else if(value != QString()) + { + mSettingDefinitions->setValue (viewKey, QStringList() << value); + return value; + } + + return QString(); +} + +bool CSMSettings::UserSettings::hasSettingDefinitions (const QString &viewKey) const { return (mSettingDefinitions->contains (viewKey)); } @@ -326,10 +446,12 @@ void CSMSettings::UserSettings::saveDefinitions() const QString CSMSettings::UserSettings::settingValue (const QString &settingKey) { + QStringList defs; + if (!mSettingDefinitions->contains (settingKey)) return QString(); - QStringList defs = mSettingDefinitions->value (settingKey).toStringList(); + defs = mSettingDefinitions->value (settingKey).toStringList(); if (defs.isEmpty()) return QString(); @@ -339,8 +461,8 @@ QString CSMSettings::UserSettings::settingValue (const QString &settingKey) CSMSettings::UserSettings& CSMSettings::UserSettings::instance() { - assert(mUserSettingsInstance); - return *mUserSettingsInstance; + assert(sUserSettingsInstance); + return *sUserSettingsInstance; } void CSMSettings::UserSettings::updateUserSetting(const QString &settingKey, @@ -348,6 +470,15 @@ void CSMSettings::UserSettings::updateUserSetting(const QString &settingKey, { mSettingDefinitions->setValue (settingKey ,list); + if(settingKey == "3d-render-adv/num_lights" && !list.empty()) + { + sh::Factory::getInstance ().setGlobalSetting ("num_lights", list.at(0).toStdString()); + } + else if(settingKey == "3d-render/shaders" && !list.empty()) + { + sh::Factory::getInstance ().setShadersEnabled (list.at(0).toStdString() == "true" ? true : false); + } + emit userSettingUpdated (settingKey, list); } @@ -387,30 +518,62 @@ void CSMSettings::UserSettings::removeSetting } } - CSMSettings::SettingPageMap CSMSettings::UserSettings::settingPageMap() const { SettingPageMap pageMap; foreach (Setting *setting, mSettings) - pageMap[setting->page()].append (setting); + { + SettingPageMap::iterator iter = pageMap.find (setting->page()); + + if (iter==pageMap.end()) + { + QPair > value; + + std::map::const_iterator iter2 = + mSectionLabels.find (setting->page()); + + value.first = iter2!=mSectionLabels.end() ? iter2->second : ""; + + iter = pageMap.insert (setting->page(), value); + } + + iter->second.append (setting); + } return pageMap; } CSMSettings::Setting *CSMSettings::UserSettings::createSetting - (CSMSettings::SettingType typ, const QString &page, const QString &name) + (CSMSettings::SettingType type, const QString &name, const QString& label) { - //get list of all settings for the current setting name - if (findSetting (page, name)) - { - qWarning() << "Duplicate declaration encountered: " - << (name + '/' + page); - return 0; - } + Setting *setting = new Setting (type, name, mSection, label); + + // set useful defaults + int row = 1; + + if (!mSettings.empty()) + row = mSettings.back()->viewRow()+1; + + setting->setViewLocation (row, 1); + + setting->setColumnSpan (3); - Setting *setting = new Setting (typ, name, page); + int width = 10; + if (type==Type_CheckBox) + width = 40; + + setting->setWidgetWidth (width); + + if (type==Type_CheckBox) + setting->setStyleSheet ("QGroupBox { border: 0px; }"); + + if (type==Type_CheckBox) + setting->setDeclaredValues(QStringList() << "true" << "false"); + + if (type==Type_CheckBox) + setting->setSpecialValueText (setting->getLabel()); //add declaration to the model mSettings.append (setting); @@ -418,6 +581,12 @@ CSMSettings::Setting *CSMSettings::UserSettings::createSetting return setting; } +void CSMSettings::UserSettings::declareSection (const QString& page, const QString& label) +{ + mSection = page; + mSectionLabels[page] = label; +} + QStringList CSMSettings::UserSettings::definitions (const QString &viewKey) const { if (mSettingDefinitions->contains (viewKey)) diff --git a/apps/opencs/model/settings/usersettings.hpp b/apps/opencs/model/settings/usersettings.hpp index 7e553caf6..5188a9842 100644 --- a/apps/opencs/model/settings/usersettings.hpp +++ b/apps/opencs/model/settings/usersettings.hpp @@ -1,10 +1,13 @@ #ifndef USERSETTINGS_HPP #define USERSETTINGS_HPP +#include + #include #include #include #include +#include #include #include "support.hpp" @@ -22,18 +25,20 @@ class QSettings; namespace CSMSettings { class Setting; - typedef QMap > SettingPageMap; + typedef QMap > > SettingPageMap; class UserSettings: public QObject { Q_OBJECT - static UserSettings *mUserSettingsInstance; + static UserSettings *sUserSettingsInstance; const Files::ConfigurationManager& mCfgMgr; QSettings *mSettingDefinitions; QList mSettings; + QString mSection; + std::map mSectionLabels; public: @@ -62,7 +67,7 @@ namespace CSMSettings { void removeSetting (const QString &pageName, const QString &settingName); - ///Retreive a map of the settings, keyed by page name + ///Retrieve a map of the settings, keyed by page name SettingPageMap settingPageMap() const; ///Returns a string list of defined vlaues for the specified setting @@ -75,13 +80,20 @@ namespace CSMSettings { ///Save any unsaved changes in the QSettings object void saveDefinitions() const; + QString setting(const QString &viewKey, const QString &value = QString()); + private: void buildSettingModelDefaults(); ///add a new setting to the model and return it - Setting *createSetting (CSMSettings::SettingType typ, - const QString &page, const QString &name); + Setting *createSetting (CSMSettings::SettingType type, const QString &name, + const QString& label); + + /// Set the section for createSetting calls. + /// + /// Sections can be declared multiple times. + void declareSection (const QString& page, const QString& label); signals: diff --git a/apps/opencs/model/tools/birthsigncheck.cpp b/apps/opencs/model/tools/birthsigncheck.cpp index db20ce4bc..1d72e24b8 100644 --- a/apps/opencs/model/tools/birthsigncheck.cpp +++ b/apps/opencs/model/tools/birthsigncheck.cpp @@ -17,7 +17,7 @@ int CSMTools::BirthsignCheckStage::setup() return mBirthsigns.getSize(); } -void CSMTools::BirthsignCheckStage::perform (int stage, Messages& messages) +void CSMTools::BirthsignCheckStage::perform (int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mBirthsigns.getRecord (stage); diff --git a/apps/opencs/model/tools/birthsigncheck.hpp b/apps/opencs/model/tools/birthsigncheck.hpp index 1030e5c02..16d4c666f 100644 --- a/apps/opencs/model/tools/birthsigncheck.hpp +++ b/apps/opencs/model/tools/birthsigncheck.hpp @@ -21,7 +21,7 @@ namespace CSMTools virtual int setup(); ///< \return number of steps - virtual void perform (int stage, Messages& messages); + virtual void perform (int stage, CSMDoc::Messages& messages); ///< Messages resulting from this tage will be appended to \a messages. }; } diff --git a/apps/opencs/model/tools/bodypartcheck.cpp b/apps/opencs/model/tools/bodypartcheck.cpp new file mode 100644 index 000000000..68a09485f --- /dev/null +++ b/apps/opencs/model/tools/bodypartcheck.cpp @@ -0,0 +1,51 @@ +#include "bodypartcheck.hpp" + +CSMTools::BodyPartCheckStage::BodyPartCheckStage( + const CSMWorld::IdCollection &bodyParts, + const CSMWorld::Resources &meshes, + const CSMWorld::IdCollection &races ) : + mBodyParts(bodyParts), + mMeshes(meshes), + mRaces(races) +{ } + +int CSMTools::BodyPartCheckStage::setup() +{ + return mBodyParts.getSize(); +} + +void CSMTools::BodyPartCheckStage::perform (int stage, CSMDoc::Messages &messages) +{ + const CSMWorld::Record &record = mBodyParts.getRecord(stage); + + if ( record.isDeleted() ) + return; + + const ESM::BodyPart &bodyPart = record.get(); + + CSMWorld::UniversalId id( CSMWorld::UniversalId::Type_BodyPart, bodyPart.mId ); + + // Check BYDT + if (bodyPart.mData.mPart > 14 ) + messages.push_back(std::make_pair( id, bodyPart.mId + " has out of range part value." )); + + if (bodyPart.mData.mFlags > 3 ) + messages.push_back(std::make_pair( id, bodyPart.mId + " has out of range flags value." )); + + if (bodyPart.mData.mType > 2 ) + messages.push_back(std::make_pair( id, bodyPart.mId + " has out of range type value." )); + + // Check MODL + + if ( bodyPart.mModel.empty() ) + messages.push_back(std::make_pair( id, bodyPart.mId + " has no model." )); + else if ( mMeshes.searchId( bodyPart.mModel ) == -1 ) + messages.push_back(std::make_pair( id, bodyPart.mId + " has invalid model." )); + + // Check FNAM + + if ( bodyPart.mRace.empty() ) + messages.push_back(std::make_pair( id, bodyPart.mId + " has no race." )); + else if ( mRaces.searchId( bodyPart.mRace ) == -1 ) + messages.push_back(std::make_pair( id, bodyPart.mId + " has invalid race." )); +} diff --git a/apps/opencs/model/tools/bodypartcheck.hpp b/apps/opencs/model/tools/bodypartcheck.hpp new file mode 100644 index 000000000..0a6ca959a --- /dev/null +++ b/apps/opencs/model/tools/bodypartcheck.hpp @@ -0,0 +1,35 @@ +#ifndef CSM_TOOLS_BODYPARTCHECK_H +#define CSM_TOOLS_BODYPARTCHECK_H + +#include +#include + +#include "../world/resources.hpp" +#include "../world/idcollection.hpp" + +#include "../doc/stage.hpp" + +namespace CSMTools +{ + /// \brief VerifyStage: make sure that body part records are internally consistent + class BodyPartCheckStage : public CSMDoc::Stage + { + const CSMWorld::IdCollection &mBodyParts; + const CSMWorld::Resources &mMeshes; + const CSMWorld::IdCollection &mRaces; + + public: + BodyPartCheckStage( + const CSMWorld::IdCollection &bodyParts, + const CSMWorld::Resources &meshes, + const CSMWorld::IdCollection &races ); + + virtual int setup(); + ///< \return number of steps + + virtual void perform( int stage, CSMDoc::Messages &messages ); + ///< Messages resulting from this tage will be appended to \a messages. + }; +} + +#endif \ No newline at end of file diff --git a/apps/opencs/model/tools/classcheck.cpp b/apps/opencs/model/tools/classcheck.cpp index cea4f3a68..5b872a266 100644 --- a/apps/opencs/model/tools/classcheck.cpp +++ b/apps/opencs/model/tools/classcheck.cpp @@ -18,7 +18,7 @@ int CSMTools::ClassCheckStage::setup() return mClasses.getSize(); } -void CSMTools::ClassCheckStage::perform (int stage, Messages& messages) +void CSMTools::ClassCheckStage::perform (int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mClasses.getRecord (stage); diff --git a/apps/opencs/model/tools/classcheck.hpp b/apps/opencs/model/tools/classcheck.hpp index ec50ba35d..b76da3f13 100644 --- a/apps/opencs/model/tools/classcheck.hpp +++ b/apps/opencs/model/tools/classcheck.hpp @@ -21,7 +21,7 @@ namespace CSMTools virtual int setup(); ///< \return number of steps - virtual void perform (int stage, Messages& messages); + virtual void perform (int stage, CSMDoc::Messages& messages); ///< Messages resulting from this tage will be appended to \a messages. }; } diff --git a/apps/opencs/model/tools/factioncheck.cpp b/apps/opencs/model/tools/factioncheck.cpp index ba8cfe1f9..0dfdee775 100644 --- a/apps/opencs/model/tools/factioncheck.cpp +++ b/apps/opencs/model/tools/factioncheck.cpp @@ -18,7 +18,7 @@ int CSMTools::FactionCheckStage::setup() return mFactions.getSize(); } -void CSMTools::FactionCheckStage::perform (int stage, Messages& messages) +void CSMTools::FactionCheckStage::perform (int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mFactions.getRecord (stage); diff --git a/apps/opencs/model/tools/factioncheck.hpp b/apps/opencs/model/tools/factioncheck.hpp index ccc44e6a9..321a4d6d8 100644 --- a/apps/opencs/model/tools/factioncheck.hpp +++ b/apps/opencs/model/tools/factioncheck.hpp @@ -21,7 +21,7 @@ namespace CSMTools virtual int setup(); ///< \return number of steps - virtual void perform (int stage, Messages& messages); + virtual void perform (int stage, CSMDoc::Messages& messages); ///< Messages resulting from this tage will be appended to \a messages. }; } diff --git a/apps/opencs/model/tools/mandatoryid.cpp b/apps/opencs/model/tools/mandatoryid.cpp index 412e9f2f0..87d19401b 100644 --- a/apps/opencs/model/tools/mandatoryid.cpp +++ b/apps/opencs/model/tools/mandatoryid.cpp @@ -15,10 +15,9 @@ int CSMTools::MandatoryIdStage::setup() return mIds.size(); } -void CSMTools::MandatoryIdStage::perform (int stage, Messages& messages) +void CSMTools::MandatoryIdStage::perform (int stage, CSMDoc::Messages& messages) { if (mIdCollection.searchId (mIds.at (stage))==-1 || mIdCollection.getRecord (mIds.at (stage)).isDeleted()) - messages.push_back (std::make_pair (mCollectionId, - "Missing mandatory record: " + mIds.at (stage))); + messages.add (mCollectionId, "Missing mandatory record: " + mIds.at (stage)); } \ No newline at end of file diff --git a/apps/opencs/model/tools/mandatoryid.hpp b/apps/opencs/model/tools/mandatoryid.hpp index a8afea62a..86015c982 100644 --- a/apps/opencs/model/tools/mandatoryid.hpp +++ b/apps/opencs/model/tools/mandatoryid.hpp @@ -30,7 +30,7 @@ namespace CSMTools virtual int setup(); ///< \return number of steps - virtual void perform (int stage, Messages& messages); + virtual void perform (int stage, CSMDoc::Messages& messages); ///< Messages resulting from this tage will be appended to \a messages. }; } diff --git a/apps/opencs/model/tools/racecheck.cpp b/apps/opencs/model/tools/racecheck.cpp index 47aeda1e6..143d61772 100644 --- a/apps/opencs/model/tools/racecheck.cpp +++ b/apps/opencs/model/tools/racecheck.cpp @@ -7,7 +7,7 @@ #include "../world/universalid.hpp" -void CSMTools::RaceCheckStage::performPerRecord (int stage, Messages& messages) +void CSMTools::RaceCheckStage::performPerRecord (int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mRaces.getRecord (stage); @@ -46,7 +46,7 @@ void CSMTools::RaceCheckStage::performPerRecord (int stage, Messages& messages) /// \todo check data members that can't be edited in the table view } -void CSMTools::RaceCheckStage::performFinal (Messages& messages) +void CSMTools::RaceCheckStage::performFinal (CSMDoc::Messages& messages) { CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Races); @@ -64,7 +64,7 @@ int CSMTools::RaceCheckStage::setup() return mRaces.getSize()+1; } -void CSMTools::RaceCheckStage::perform (int stage, Messages& messages) +void CSMTools::RaceCheckStage::perform (int stage, CSMDoc::Messages& messages) { if (stage==mRaces.getSize()) performFinal (messages); diff --git a/apps/opencs/model/tools/racecheck.hpp b/apps/opencs/model/tools/racecheck.hpp index c68b283be..3e67b7577 100644 --- a/apps/opencs/model/tools/racecheck.hpp +++ b/apps/opencs/model/tools/racecheck.hpp @@ -15,9 +15,9 @@ namespace CSMTools const CSMWorld::IdCollection& mRaces; bool mPlayable; - void performPerRecord (int stage, Messages& messages); + void performPerRecord (int stage, CSMDoc::Messages& messages); - void performFinal (Messages& messages); + void performFinal (CSMDoc::Messages& messages); public: @@ -26,7 +26,7 @@ namespace CSMTools virtual int setup(); ///< \return number of steps - virtual void perform (int stage, Messages& messages); + virtual void perform (int stage, CSMDoc::Messages& messages); ///< Messages resulting from this tage will be appended to \a messages. }; } diff --git a/apps/opencs/model/tools/referenceablecheck.cpp b/apps/opencs/model/tools/referenceablecheck.cpp index 1816d0808..5190aacd5 100644 --- a/apps/opencs/model/tools/referenceablecheck.cpp +++ b/apps/opencs/model/tools/referenceablecheck.cpp @@ -18,7 +18,7 @@ CSMTools::ReferenceableCheckStage::ReferenceableCheckStage( { } -void CSMTools::ReferenceableCheckStage::perform (int stage, Messages& messages) +void CSMTools::ReferenceableCheckStage::perform (int stage, CSMDoc::Messages& messages) { //Checks for books, than, when stage is above mBooksSize goes to other checks, with (stage - PrevSum) as stage. const int bookSize(mReferencables.getBooks().getSize()); @@ -232,7 +232,7 @@ int CSMTools::ReferenceableCheckStage::setup() void CSMTools::ReferenceableCheckStage::bookCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::Book >& records, - Messages& messages) + CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -250,7 +250,7 @@ void CSMTools::ReferenceableCheckStage::bookCheck( void CSMTools::ReferenceableCheckStage::activatorCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::Activator >& records, - Messages& messages) + CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -270,7 +270,7 @@ void CSMTools::ReferenceableCheckStage::activatorCheck( void CSMTools::ReferenceableCheckStage::potionCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::Potion >& records, - Messages& messages) + CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -290,7 +290,7 @@ void CSMTools::ReferenceableCheckStage::potionCheck( void CSMTools::ReferenceableCheckStage::apparatusCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::Apparatus >& records, - Messages& messages) + CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -310,7 +310,7 @@ void CSMTools::ReferenceableCheckStage::apparatusCheck( void CSMTools::ReferenceableCheckStage::armorCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::Armor >& records, - Messages& messages) + CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -336,7 +336,7 @@ void CSMTools::ReferenceableCheckStage::armorCheck( void CSMTools::ReferenceableCheckStage::clothingCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::Clothing >& records, - Messages& messages) + CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -353,7 +353,7 @@ void CSMTools::ReferenceableCheckStage::clothingCheck( void CSMTools::ReferenceableCheckStage::containerCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::Container >& records, - Messages& messages) + CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -381,7 +381,7 @@ void CSMTools::ReferenceableCheckStage::containerCheck( void CSMTools::ReferenceableCheckStage::creatureCheck ( int stage, const CSMWorld::RefIdDataContainer< ESM::Creature >& records, - Messages& messages) + CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -448,7 +448,7 @@ void CSMTools::ReferenceableCheckStage::creatureCheck ( void CSMTools::ReferenceableCheckStage::doorCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::Door >& records, - Messages& messages) + CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -469,7 +469,7 @@ void CSMTools::ReferenceableCheckStage::doorCheck( void CSMTools::ReferenceableCheckStage::ingredientCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::Ingredient >& records, - Messages& messages) + CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -487,7 +487,7 @@ void CSMTools::ReferenceableCheckStage::ingredientCheck( void CSMTools::ReferenceableCheckStage::creaturesLevListCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::CreatureLevList >& records, - Messages& messages) + CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -505,7 +505,7 @@ void CSMTools::ReferenceableCheckStage::creaturesLevListCheck( void CSMTools::ReferenceableCheckStage::itemLevelledListCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::ItemLevList >& records, - Messages& messages) + CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -522,7 +522,7 @@ void CSMTools::ReferenceableCheckStage::itemLevelledListCheck( void CSMTools::ReferenceableCheckStage::lightCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::Light >& records, - Messages& messages) + CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -547,7 +547,7 @@ void CSMTools::ReferenceableCheckStage::lightCheck( void CSMTools::ReferenceableCheckStage::lockpickCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::Lockpick >& records, - Messages& messages) + CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -567,7 +567,7 @@ void CSMTools::ReferenceableCheckStage::lockpickCheck( void CSMTools::ReferenceableCheckStage::miscCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::Miscellaneous >& records, - Messages& messages) + CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -584,7 +584,7 @@ void CSMTools::ReferenceableCheckStage::miscCheck( void CSMTools::ReferenceableCheckStage::npcCheck ( int stage, const CSMWorld::RefIdDataContainer< ESM::NPC >& records, - Messages& messages) + CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -701,7 +701,7 @@ void CSMTools::ReferenceableCheckStage::npcCheck ( void CSMTools::ReferenceableCheckStage::weaponCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::Weapon >& records, - Messages& messages) + CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord (stage); @@ -778,7 +778,7 @@ void CSMTools::ReferenceableCheckStage::weaponCheck( void CSMTools::ReferenceableCheckStage::probeCheck( int stage, const CSMWorld::RefIdDataContainer< ESM::Probe >& records, - Messages& messages) + CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); @@ -796,7 +796,7 @@ void CSMTools::ReferenceableCheckStage::probeCheck( void CSMTools::ReferenceableCheckStage::repairCheck ( int stage, const CSMWorld::RefIdDataContainer< ESM::Repair >& records, - Messages& messages) + CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord (stage); @@ -812,7 +812,7 @@ void CSMTools::ReferenceableCheckStage::repairCheck ( void CSMTools::ReferenceableCheckStage::staticCheck ( int stage, const CSMWorld::RefIdDataContainer< ESM::Static >& records, - Messages& messages) + CSMDoc::Messages& messages) { const CSMWorld::RecordBase& baseRecord = records.getRecord (stage); @@ -828,7 +828,7 @@ void CSMTools::ReferenceableCheckStage::staticCheck ( //final check -void CSMTools::ReferenceableCheckStage::finalCheck (Messages& messages) +void CSMTools::ReferenceableCheckStage::finalCheck (CSMDoc::Messages& messages) { if (!mPlayerPresent) messages.push_back (std::make_pair (CSMWorld::UniversalId::Type_Referenceables, @@ -839,7 +839,7 @@ void CSMTools::ReferenceableCheckStage::finalCheck (Messages& messages) //Templates begins here template void CSMTools::ReferenceableCheckStage::inventoryItemCheck ( - const Item& someItem, Messages& messages, const std::string& someID, bool enchantable) + const Item& someItem, CSMDoc::Messages& messages, const std::string& someID, bool enchantable) { if (someItem.mName.empty()) messages.push_back (std::make_pair (someID, someItem.mId + " has an empty name")); @@ -865,7 +865,7 @@ template void CSMTools::ReferenceableCheckStage::inventoryItemChe } template void CSMTools::ReferenceableCheckStage::inventoryItemCheck ( - const Item& someItem, Messages& messages, const std::string& someID) + const Item& someItem, CSMDoc::Messages& messages, const std::string& someID) { if (someItem.mName.empty()) messages.push_back (std::make_pair (someID, someItem.mId + " has an empty name")); @@ -888,7 +888,7 @@ template void CSMTools::ReferenceableCheckStage::inventoryItemChe } template void CSMTools::ReferenceableCheckStage::toolCheck ( - const Tool& someTool, Messages& messages, const std::string& someID, bool canBeBroken) + const Tool& someTool, CSMDoc::Messages& messages, const std::string& someID, bool canBeBroken) { if (someTool.mData.mQuality <= 0) messages.push_back (std::make_pair (someID, someTool.mId + " has non-positive quality")); @@ -899,14 +899,14 @@ template void CSMTools::ReferenceableCheckStage::toolCheck ( } template void CSMTools::ReferenceableCheckStage::toolCheck ( - const Tool& someTool, Messages& messages, const std::string& someID) + const Tool& someTool, CSMDoc::Messages& messages, const std::string& someID) { if (someTool.mData.mQuality <= 0) messages.push_back (std::make_pair (someID, someTool.mId + " has non-positive quality")); } template void CSMTools::ReferenceableCheckStage::listCheck ( - const List& someList, Messages& messages, const std::string& someID) + const List& someList, CSMDoc::Messages& messages, const std::string& someID) { for (unsigned i = 0; i < someList.mList.size(); ++i) { diff --git a/apps/opencs/model/tools/referenceablecheck.hpp b/apps/opencs/model/tools/referenceablecheck.hpp index b0129fc2a..ac7ed7082 100644 --- a/apps/opencs/model/tools/referenceablecheck.hpp +++ b/apps/opencs/model/tools/referenceablecheck.hpp @@ -17,56 +17,56 @@ namespace CSMTools const CSMWorld::IdCollection& classes, const CSMWorld::IdCollection& factions); - virtual void perform(int stage, Messages& messages); + virtual void perform(int stage, CSMDoc::Messages& messages); virtual int setup(); private: //CONCRETE CHECKS - void bookCheck(int stage, const CSMWorld::RefIdDataContainer< ESM::Book >& records, Messages& messages); - void activatorCheck(int stage, const CSMWorld::RefIdDataContainer< ESM::Activator >& records, Messages& messages); - void potionCheck(int stage, const CSMWorld::RefIdDataContainer& records, Messages& messages); - void apparatusCheck(int stage, const CSMWorld::RefIdDataContainer& records, Messages& messages); - void armorCheck(int stage, const CSMWorld::RefIdDataContainer& records, Messages& messages); - void clothingCheck(int stage, const CSMWorld::RefIdDataContainer& records, Messages& messages); - void containerCheck(int stage, const CSMWorld::RefIdDataContainer& records, Messages& messages); - void creatureCheck(int stage, const CSMWorld::RefIdDataContainer& records, Messages& messages); - void doorCheck(int stage, const CSMWorld::RefIdDataContainer& records, Messages& messages); - void ingredientCheck(int stage, const CSMWorld::RefIdDataContainer& records, Messages& messages); - void creaturesLevListCheck(int stage, const CSMWorld::RefIdDataContainer& records, Messages& messages); - void itemLevelledListCheck(int stage, const CSMWorld::RefIdDataContainer& records, Messages& messages); - void lightCheck(int stage, const CSMWorld::RefIdDataContainer& records, Messages& messages); - void lockpickCheck(int stage, const CSMWorld::RefIdDataContainer& records, Messages& messages); - void miscCheck(int stage, const CSMWorld::RefIdDataContainer& records, Messages& messages); - void npcCheck(int stage, const CSMWorld::RefIdDataContainer& records, Messages& messages); - void weaponCheck(int stage, const CSMWorld::RefIdDataContainer& records, Messages& messages); - void probeCheck(int stage, const CSMWorld::RefIdDataContainer& records, Messages& messages); - void repairCheck(int stage, const CSMWorld::RefIdDataContainer& records, Messages& messages); - void staticCheck(int stage, const CSMWorld::RefIdDataContainer& records, Messages& messages); + void bookCheck(int stage, const CSMWorld::RefIdDataContainer< ESM::Book >& records, CSMDoc::Messages& messages); + void activatorCheck(int stage, const CSMWorld::RefIdDataContainer< ESM::Activator >& records, CSMDoc::Messages& messages); + void potionCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); + void apparatusCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); + void armorCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); + void clothingCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); + void containerCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); + void creatureCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); + void doorCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); + void ingredientCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); + void creaturesLevListCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); + void itemLevelledListCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); + void lightCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); + void lockpickCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); + void miscCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); + void npcCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); + void weaponCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); + void probeCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); + void repairCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); + void staticCheck(int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages); //FINAL CHECK - void finalCheck (Messages& messages); + void finalCheck (CSMDoc::Messages& messages); //TEMPLATE CHECKS template void inventoryItemCheck(const ITEM& someItem, - Messages& messages, + CSMDoc::Messages& messages, const std::string& someID, bool enchantable); //for all enchantable items. template void inventoryItemCheck(const ITEM& someItem, - Messages& messages, + CSMDoc::Messages& messages, const std::string& someID); //for non-enchantable items. template void toolCheck(const TOOL& someTool, - Messages& messages, + CSMDoc::Messages& messages, const std::string& someID, bool canbebroken); //for tools with uses. template void toolCheck(const TOOL& someTool, - Messages& messages, + CSMDoc::Messages& messages, const std::string& someID); //for tools without uses. template void listCheck(const LIST& someList, - Messages& messages, + CSMDoc::Messages& messages, const std::string& someID); const CSMWorld::RefIdData& mReferencables; diff --git a/apps/opencs/model/tools/referencecheck.cpp b/apps/opencs/model/tools/referencecheck.cpp new file mode 100644 index 000000000..68de87d80 --- /dev/null +++ b/apps/opencs/model/tools/referencecheck.cpp @@ -0,0 +1,110 @@ +#include "referencecheck.hpp" + +#include + +CSMTools::ReferenceCheckStage::ReferenceCheckStage( + const CSMWorld::RefCollection& references, + const CSMWorld::RefIdCollection& referencables, + const CSMWorld::IdCollection& cells, + const CSMWorld::IdCollection& factions) + : + mReferences(references), + mReferencables(referencables), + mDataSet(referencables.getDataSet()), + mCells(cells), + mFactions(factions) +{ +} + +void CSMTools::ReferenceCheckStage::perform(int stage, CSMDoc::Messages &messages) +{ + const CSMWorld::Record& record = mReferences.getRecord(stage); + + if (record.isDeleted()) + return; + + const CSMWorld::CellRef& cellRef = record.get(); + const CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Reference, cellRef.mId); + + // Check for empty reference id + if (cellRef.mRefID.empty()) { + messages.push_back(std::make_pair(id, " is an empty reference")); + } else { + // Check for non existing referenced object + if (mReferencables.searchId(cellRef.mRefID) == -1) { + messages.push_back(std::make_pair(id, " is referencing non existing object " + cellRef.mRefID)); + } else { + // Check if reference charge is valid for it's proper referenced type + CSMWorld::RefIdData::LocalIndex localIndex = mDataSet.searchId(cellRef.mRefID); + bool isLight = localIndex.second == CSMWorld::UniversalId::Type_Light; + if ((isLight && cellRef.mChargeFloat < -1) || (!isLight && cellRef.mChargeInt < -1)) { + std::string str = " has invalid charge "; + if (localIndex.second == CSMWorld::UniversalId::Type_Light) + str += boost::lexical_cast(cellRef.mChargeFloat); + else + str += boost::lexical_cast(cellRef.mChargeInt); + messages.push_back(std::make_pair(id, id.getId() + str)); + } + } + } + + // Check if referenced object is in valid cell + if (mCells.searchId(cellRef.mCell) == -1) + messages.push_back(std::make_pair(id, " is referencing object from non existing cell " + cellRef.mCell)); + + // If object have owner, check if that owner reference is valid + if (!cellRef.mOwner.empty() && mReferencables.searchId(cellRef.mOwner) == -1) + messages.push_back(std::make_pair(id, " has non existing owner " + cellRef.mOwner)); + + // If object have creature soul trapped, check if that creature reference is valid + if (!cellRef.mSoul.empty()) + if (mReferencables.searchId(cellRef.mSoul) == -1) + messages.push_back(std::make_pair(id, " has non existing trapped soul " + cellRef.mSoul)); + + bool hasFaction = !cellRef.mFaction.empty(); + + // If object have faction, check if that faction reference is valid + if (hasFaction) + if (mFactions.searchId(cellRef.mFaction) == -1) + messages.push_back(std::make_pair(id, " has non existing faction " + cellRef.mFaction)); + + // Check item's faction rank + if (hasFaction && cellRef.mFactionRank < -1) + messages.push_back(std::make_pair(id, " has faction set but has invalid faction rank " + boost::lexical_cast(cellRef.mFactionRank))); + else if (!hasFaction && cellRef.mFactionRank != -2) + messages.push_back(std::make_pair(id, " has invalid faction rank " + boost::lexical_cast(cellRef.mFactionRank))); + + // If door have destination cell, check if that reference is valid + if (!cellRef.mDestCell.empty()) + if (mCells.searchId(cellRef.mDestCell) == -1) + messages.push_back(std::make_pair(id, " has non existing destination cell " + cellRef.mDestCell)); + + // Check if scale isn't negative + if (cellRef.mScale < 0) + { + std::string str = " has negative scale "; + str += boost::lexical_cast(cellRef.mScale); + messages.push_back(std::make_pair(id, id.getId() + str)); + } + + // Check if enchantement points aren't negative or are at full (-1) + if (cellRef.mEnchantmentCharge < 0 && cellRef.mEnchantmentCharge != -1) + { + std::string str = " has negative enchantment points "; + str += boost::lexical_cast(cellRef.mEnchantmentCharge); + messages.push_back(std::make_pair(id, id.getId() + str)); + } + + // Check if gold value isn't negative + if (cellRef.mGoldValue < 0) + { + std::string str = " has negative gold value "; + str += cellRef.mGoldValue; + messages.push_back(std::make_pair(id, id.getId() + str)); + } +} + +int CSMTools::ReferenceCheckStage::setup() +{ + return mReferences.getSize(); +} diff --git a/apps/opencs/model/tools/referencecheck.hpp b/apps/opencs/model/tools/referencecheck.hpp new file mode 100644 index 000000000..9cf685b3a --- /dev/null +++ b/apps/opencs/model/tools/referencecheck.hpp @@ -0,0 +1,29 @@ +#ifndef CSM_TOOLS_REFERENCECHECK_H +#define CSM_TOOLS_REFERENCECHECK_H + +#include "../doc/state.hpp" +#include "../doc/document.hpp" + +namespace CSMTools +{ + class ReferenceCheckStage : public CSMDoc::Stage + { + public: + ReferenceCheckStage (const CSMWorld::RefCollection& references, + const CSMWorld::RefIdCollection& referencables, + const CSMWorld::IdCollection& cells, + const CSMWorld::IdCollection& factions); + + virtual void perform(int stage, CSMDoc::Messages& messages); + virtual int setup(); + + private: + const CSMWorld::RefCollection& mReferences; + const CSMWorld::RefIdCollection& mReferencables; + const CSMWorld::RefIdData& mDataSet; + const CSMWorld::IdCollection& mCells; + const CSMWorld::IdCollection& mFactions; + }; +} + +#endif // CSM_TOOLS_REFERENCECHECK_H \ No newline at end of file diff --git a/apps/opencs/model/tools/regioncheck.cpp b/apps/opencs/model/tools/regioncheck.cpp index 07df20470..091836d0d 100644 --- a/apps/opencs/model/tools/regioncheck.cpp +++ b/apps/opencs/model/tools/regioncheck.cpp @@ -17,7 +17,7 @@ int CSMTools::RegionCheckStage::setup() return mRegions.getSize(); } -void CSMTools::RegionCheckStage::perform (int stage, Messages& messages) +void CSMTools::RegionCheckStage::perform (int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mRegions.getRecord (stage); diff --git a/apps/opencs/model/tools/regioncheck.hpp b/apps/opencs/model/tools/regioncheck.hpp index a12903e7d..8ba32e137 100644 --- a/apps/opencs/model/tools/regioncheck.hpp +++ b/apps/opencs/model/tools/regioncheck.hpp @@ -21,7 +21,7 @@ namespace CSMTools virtual int setup(); ///< \return number of steps - virtual void perform (int stage, Messages& messages); + virtual void perform (int stage, CSMDoc::Messages& messages); ///< Messages resulting from this tage will be appended to \a messages. }; } diff --git a/apps/opencs/model/tools/reportmodel.cpp b/apps/opencs/model/tools/reportmodel.cpp index 75545a7c7..135420612 100644 --- a/apps/opencs/model/tools/reportmodel.cpp +++ b/apps/opencs/model/tools/reportmodel.cpp @@ -16,7 +16,7 @@ int CSMTools::ReportModel::columnCount (const QModelIndex & parent) const if (parent.isValid()) return 0; - return 2; + return 3; } QVariant CSMTools::ReportModel::data (const QModelIndex & index, int role) const @@ -26,8 +26,11 @@ QVariant CSMTools::ReportModel::data (const QModelIndex & index, int role) const if (index.column()==0) return static_cast (mRows.at (index.row()).first.getType()); - else - return mRows.at (index.row()).second.c_str(); + + if (index.column()==1) + return QString::fromUtf8 (mRows.at (index.row()).second.first.c_str()); + + return QString::fromUtf8 (mRows.at (index.row()).second.second.c_str()); } QVariant CSMTools::ReportModel::headerData (int section, Qt::Orientation orientation, int role) const @@ -38,7 +41,13 @@ QVariant CSMTools::ReportModel::headerData (int section, Qt::Orientation orienta if (orientation==Qt::Vertical) return QVariant(); - return tr (section==0 ? "Type" : "Description"); + if (section==0) + return "Type"; + + if (section==1) + return "Description"; + + return "Hint"; } bool CSMTools::ReportModel::removeRows (int row, int count, const QModelIndex& parent) @@ -51,11 +60,12 @@ bool CSMTools::ReportModel::removeRows (int row, int count, const QModelIndex& p return true; } -void CSMTools::ReportModel::add (const CSMWorld::UniversalId& id, const std::string& message) +void CSMTools::ReportModel::add (const CSMWorld::UniversalId& id, const std::string& message, + const std::string& hint) { beginInsertRows (QModelIndex(), mRows.size(), mRows.size()); - mRows.push_back (std::make_pair (id, message)); + mRows.push_back (std::make_pair (id, std::make_pair (message, hint))); endInsertRows(); } @@ -64,3 +74,8 @@ const CSMWorld::UniversalId& CSMTools::ReportModel::getUniversalId (int row) con { return mRows.at (row).first; } + +std::string CSMTools::ReportModel::getHint (int row) const +{ + return mRows.at (row).second.second; +} \ No newline at end of file diff --git a/apps/opencs/model/tools/reportmodel.hpp b/apps/opencs/model/tools/reportmodel.hpp index 0f000245e..709e024a7 100644 --- a/apps/opencs/model/tools/reportmodel.hpp +++ b/apps/opencs/model/tools/reportmodel.hpp @@ -14,7 +14,7 @@ namespace CSMTools { Q_OBJECT - std::vector > mRows; + std::vector > > mRows; public: @@ -28,9 +28,12 @@ namespace CSMTools virtual bool removeRows (int row, int count, const QModelIndex& parent = QModelIndex()); - void add (const CSMWorld::UniversalId& id, const std::string& message); + void add (const CSMWorld::UniversalId& id, const std::string& message, + const std::string& hint = ""); const CSMWorld::UniversalId& getUniversalId (int row) const; + + std::string getHint (int row) const; }; } diff --git a/apps/opencs/model/tools/scriptcheck.cpp b/apps/opencs/model/tools/scriptcheck.cpp index b989e22a2..d9dea7f43 100644 --- a/apps/opencs/model/tools/scriptcheck.cpp +++ b/apps/opencs/model/tools/scriptcheck.cpp @@ -7,6 +7,8 @@ #include #include +#include "../doc/document.hpp" + #include "../world/data.hpp" void CSMTools::ScriptCheckStage::report (const std::string& message, const Compiler::TokenLoc& loc, @@ -26,7 +28,11 @@ void CSMTools::ScriptCheckStage::report (const std::string& message, const Compi << ", line " << loc.mLine << ", column " << loc.mColumn << " (" << loc.mLiteral << "): " << message; - mMessages->push_back (std::make_pair (id, stream.str())); + std::ostringstream hintStream; + + hintStream << "l:" << loc.mLine << " " << loc.mColumn; + + mMessages->add (id, stream.str(), hintStream.str()); } void CSMTools::ScriptCheckStage::report (const std::string& message, Type type) @@ -37,8 +43,8 @@ void CSMTools::ScriptCheckStage::report (const std::string& message, Type type) (type==ErrorMessage ? "error: " : "warning: ") + message)); } -CSMTools::ScriptCheckStage::ScriptCheckStage (const CSMWorld::Data& data) -: mData (data), mContext (data), mMessages (0) +CSMTools::ScriptCheckStage::ScriptCheckStage (const CSMDoc::Document& document) +: mDocument (document), mContext (document.getData()), mMessages (0) { /// \todo add an option to configure warning mode setWarningsMode (0); @@ -53,18 +59,25 @@ int CSMTools::ScriptCheckStage::setup() mMessages = 0; mId.clear(); - return mData.getScripts().getSize(); + return mDocument.getData().getScripts().getSize(); } -void CSMTools::ScriptCheckStage::perform (int stage, Messages& messages) +void CSMTools::ScriptCheckStage::perform (int stage, CSMDoc::Messages& messages) { + mId = mDocument.getData().getScripts().getId (stage); + + if (mDocument.isBlacklisted ( + CSMWorld::UniversalId (CSMWorld::UniversalId::Type_Script, mId))) + return; + mMessages = &messages; - mId = mData.getScripts().getId (stage); try { - mFile = mData.getScripts().getRecord (stage).get().mId; - std::istringstream input (mData.getScripts().getRecord (stage).get().mScriptText); + const CSMWorld::Data& data = mDocument.getData(); + + mFile = data.getScripts().getRecord (stage).get().mId; + std::istringstream input (data.getScripts().getRecord (stage).get().mScriptText); Compiler::Scanner scanner (*this, input, mContext.getExtensions()); diff --git a/apps/opencs/model/tools/scriptcheck.hpp b/apps/opencs/model/tools/scriptcheck.hpp index ecf8d61b7..3fe12fc9a 100644 --- a/apps/opencs/model/tools/scriptcheck.hpp +++ b/apps/opencs/model/tools/scriptcheck.hpp @@ -8,17 +8,22 @@ #include "../world/scriptcontext.hpp" +namespace CSMDoc +{ + class Document; +} + namespace CSMTools { /// \brief VerifyStage: make sure that scripts compile class ScriptCheckStage : public CSMDoc::Stage, private Compiler::ErrorHandler { - const CSMWorld::Data& mData; + const CSMDoc::Document& mDocument; Compiler::Extensions mExtensions; CSMWorld::ScriptContext mContext; std::string mId; std::string mFile; - Messages *mMessages; + CSMDoc::Messages *mMessages; virtual void report (const std::string& message, const Compiler::TokenLoc& loc, Type type); ///< Report error to the user. @@ -28,12 +33,12 @@ namespace CSMTools public: - ScriptCheckStage (const CSMWorld::Data& data); + ScriptCheckStage (const CSMDoc::Document& document); virtual int setup(); ///< \return number of steps - virtual void perform (int stage, Messages& messages); + virtual void perform (int stage, CSMDoc::Messages& messages); ///< Messages resulting from this tage will be appended to \a messages. }; } diff --git a/apps/opencs/model/tools/skillcheck.cpp b/apps/opencs/model/tools/skillcheck.cpp index 630516c72..e061e042c 100644 --- a/apps/opencs/model/tools/skillcheck.cpp +++ b/apps/opencs/model/tools/skillcheck.cpp @@ -16,7 +16,7 @@ int CSMTools::SkillCheckStage::setup() return mSkills.getSize(); } -void CSMTools::SkillCheckStage::perform (int stage, Messages& messages) +void CSMTools::SkillCheckStage::perform (int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mSkills.getRecord (stage); diff --git a/apps/opencs/model/tools/skillcheck.hpp b/apps/opencs/model/tools/skillcheck.hpp index cf5d53b5c..93b06fe71 100644 --- a/apps/opencs/model/tools/skillcheck.hpp +++ b/apps/opencs/model/tools/skillcheck.hpp @@ -21,7 +21,7 @@ namespace CSMTools virtual int setup(); ///< \return number of steps - virtual void perform (int stage, Messages& messages); + virtual void perform (int stage, CSMDoc::Messages& messages); ///< Messages resulting from this tage will be appended to \a messages. }; } diff --git a/apps/opencs/model/tools/soundcheck.cpp b/apps/opencs/model/tools/soundcheck.cpp index 3d222e909..e122ced91 100644 --- a/apps/opencs/model/tools/soundcheck.cpp +++ b/apps/opencs/model/tools/soundcheck.cpp @@ -16,7 +16,7 @@ int CSMTools::SoundCheckStage::setup() return mSounds.getSize(); } -void CSMTools::SoundCheckStage::perform (int stage, Messages& messages) +void CSMTools::SoundCheckStage::perform (int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mSounds.getRecord (stage); diff --git a/apps/opencs/model/tools/soundcheck.hpp b/apps/opencs/model/tools/soundcheck.hpp index a82a0eb6d..52f2d3714 100644 --- a/apps/opencs/model/tools/soundcheck.hpp +++ b/apps/opencs/model/tools/soundcheck.hpp @@ -21,7 +21,7 @@ namespace CSMTools virtual int setup(); ///< \return number of steps - virtual void perform (int stage, Messages& messages); + virtual void perform (int stage, CSMDoc::Messages& messages); ///< Messages resulting from this tage will be appended to \a messages. }; } diff --git a/apps/opencs/model/tools/spellcheck.cpp b/apps/opencs/model/tools/spellcheck.cpp index 3d0be46fd..0b59dc862 100644 --- a/apps/opencs/model/tools/spellcheck.cpp +++ b/apps/opencs/model/tools/spellcheck.cpp @@ -17,7 +17,7 @@ int CSMTools::SpellCheckStage::setup() return mSpells.getSize(); } -void CSMTools::SpellCheckStage::perform (int stage, Messages& messages) +void CSMTools::SpellCheckStage::perform (int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mSpells.getRecord (stage); diff --git a/apps/opencs/model/tools/spellcheck.hpp b/apps/opencs/model/tools/spellcheck.hpp index 182f1888b..9c3ea8885 100644 --- a/apps/opencs/model/tools/spellcheck.hpp +++ b/apps/opencs/model/tools/spellcheck.hpp @@ -21,7 +21,7 @@ namespace CSMTools virtual int setup(); ///< \return number of steps - virtual void perform (int stage, Messages& messages); + virtual void perform (int stage, CSMDoc::Messages& messages); ///< Messages resulting from this tage will be appended to \a messages. }; } diff --git a/apps/opencs/model/tools/tools.cpp b/apps/opencs/model/tools/tools.cpp index 2f93db48e..e78758bb6 100644 --- a/apps/opencs/model/tools/tools.cpp +++ b/apps/opencs/model/tools/tools.cpp @@ -5,6 +5,7 @@ #include "../doc/state.hpp" #include "../doc/operation.hpp" +#include "../doc/document.hpp" #include "../world/data.hpp" #include "../world/universalid.hpp" @@ -21,6 +22,8 @@ #include "spellcheck.hpp" #include "referenceablecheck.hpp" #include "scriptcheck.hpp" +#include "bodypartcheck.hpp" +#include "referencecheck.hpp" CSMDoc::Operation *CSMTools::Tools::get (int type) { @@ -44,10 +47,10 @@ CSMDoc::Operation *CSMTools::Tools::getVerifier() mVerifier = new CSMDoc::Operation (CSMDoc::State_Verifying, false); connect (mVerifier, SIGNAL (progress (int, int, int)), this, SIGNAL (progress (int, int, int))); - connect (mVerifier, SIGNAL (done (int)), this, SIGNAL (done (int))); + connect (mVerifier, SIGNAL (done (int, bool)), this, SIGNAL (done (int, bool))); connect (mVerifier, - SIGNAL (reportMessage (const CSMWorld::UniversalId&, const std::string&, int)), - this, SLOT (verifierMessage (const CSMWorld::UniversalId&, const std::string&, int))); + SIGNAL (reportMessage (const CSMWorld::UniversalId&, const std::string&, const std::string&, int)), + this, SLOT (verifierMessage (const CSMWorld::UniversalId&, const std::string&, const std::string&, int))); std::vector mandatoryIds; // I want C++11, damn it! mandatoryIds.push_back ("Day"); @@ -55,9 +58,6 @@ CSMDoc::Operation *CSMTools::Tools::getVerifier() mandatoryIds.push_back ("GameHour"); mandatoryIds.push_back ("Month"); mandatoryIds.push_back ("PCRace"); - mandatoryIds.push_back ("PCVampire"); - mandatoryIds.push_back ("PCWerewolf"); - mandatoryIds.push_back ("PCYear"); mVerifier->appendStage (new MandatoryIdStage (mData.getGlobals(), CSMWorld::UniversalId (CSMWorld::UniversalId::Type_Globals), mandatoryIds)); @@ -78,15 +78,25 @@ CSMDoc::Operation *CSMTools::Tools::getVerifier() mVerifier->appendStage (new SpellCheckStage (mData.getSpells())); - mVerifier->appendStage (new ReferenceableCheckStage (mData.getReferenceables().getDataSet(), mData.getRaces(), mData.getClasses(), mData.getFactions())); + mVerifier->appendStage (new ReferenceableCheckStage (mData.getReferenceables().getDataSet(), mData.getRaces(), mData.getClasses(), mData.getFactions())); - mVerifier->appendStage (new ScriptCheckStage (mData)); + mVerifier->appendStage (new ReferenceCheckStage(mData.getReferences(), mData.getReferenceables(), mData.getCells(), mData.getFactions())); + + mVerifier->appendStage (new ScriptCheckStage (mDocument)); + + mVerifier->appendStage( + new BodyPartCheckStage( + mData.getBodyParts(), + mData.getResources( + CSMWorld::UniversalId( CSMWorld::UniversalId::Type_Meshes )), + mData.getRaces() )); } return mVerifier; } -CSMTools::Tools::Tools (CSMWorld::Data& data) : mData (data), mVerifier (0), mNextReportNumber (0) +CSMTools::Tools::Tools (CSMDoc::Document& document) +: mDocument (document), mData (document.getData()), mVerifier (0), mNextReportNumber (0) { // index 0: load error log mReports.insert (std::make_pair (mNextReportNumber++, new ReportModel)); @@ -145,11 +155,11 @@ CSMTools::ReportModel *CSMTools::Tools::getReport (const CSMWorld::UniversalId& } void CSMTools::Tools::verifierMessage (const CSMWorld::UniversalId& id, const std::string& message, - int type) + const std::string& hint, int type) { std::map::iterator iter = mActiveReports.find (type); if (iter!=mActiveReports.end()) - mReports[iter->second]->add (id, message); + mReports[iter->second]->add (id, message, hint); } diff --git a/apps/opencs/model/tools/tools.hpp b/apps/opencs/model/tools/tools.hpp index 3394d3f62..5125a3638 100644 --- a/apps/opencs/model/tools/tools.hpp +++ b/apps/opencs/model/tools/tools.hpp @@ -14,6 +14,7 @@ namespace CSMWorld namespace CSMDoc { class Operation; + class Document; } namespace CSMTools @@ -24,6 +25,7 @@ namespace CSMTools { Q_OBJECT + CSMDoc::Document& mDocument; CSMWorld::Data& mData; CSMDoc::Operation *mVerifier; std::map mReports; @@ -44,7 +46,7 @@ namespace CSMTools public: - Tools (CSMWorld::Data& data); + Tools (CSMDoc::Document& document); virtual ~Tools(); @@ -62,13 +64,13 @@ namespace CSMTools private slots: void verifierMessage (const CSMWorld::UniversalId& id, const std::string& message, - int type); + const std::string& hint, int type); signals: void progress (int current, int max, int type); - void done (int type); + void done (int type, bool failed); }; } diff --git a/apps/opencs/model/world/columnbase.hpp b/apps/opencs/model/world/columnbase.hpp index 05fecaec0..5926df269 100644 --- a/apps/opencs/model/world/columnbase.hpp +++ b/apps/opencs/model/world/columnbase.hpp @@ -70,6 +70,7 @@ namespace CSMWorld Display_TopicInfo, Display_JournalInfo, Display_Scene, + Display_GlobalVariable, //CONCRETE TYPES ENDS HERE Display_Integer, @@ -107,7 +108,10 @@ namespace CSMWorld Display_SoundRes, Display_Texture, Display_Video, - Display_Colour + Display_Colour, + Display_ScriptLines, // console context + Display_SoundGeneratorType, + Display_School }; int mColumnId; diff --git a/apps/opencs/model/world/columnimp.hpp b/apps/opencs/model/world/columnimp.hpp index e416c332b..dc8edbc51 100644 --- a/apps/opencs/model/world/columnimp.hpp +++ b/apps/opencs/model/world/columnimp.hpp @@ -1,7 +1,9 @@ #ifndef CSM_WOLRD_COLUMNIMP_H #define CSM_WOLRD_COLUMNIMP_H +#include #include +#include #include #include @@ -271,7 +273,7 @@ namespace CSMWorld { ESXRecordT record2 = record.get(); - record2.mData.mUseValue[mIndex] = data.toInt(); + record2.mData.mUseValue[mIndex] = data.toFloat(); record.setModified (record2); } @@ -501,6 +503,47 @@ namespace CSMWorld } }; + template + struct FlagColumn2 : public Column + { + int mMask; + bool mInverted; + + FlagColumn2 (int columnId, int mask, bool inverted = false) + : Column (columnId, ColumnBase::Display_Boolean), mMask (mask), + mInverted (inverted) + {} + + virtual QVariant get (const Record& record) const + { + bool flag = (record.get().mFlags & mMask)!=0; + + if (mInverted) + flag = !flag; + + return flag; + } + + virtual void set (Record& record, const QVariant& data) + { + ESXRecordT record2 = record.get(); + + int flags = record2.mFlags & ~mMask; + + if ((data.toInt()!=0)!=mInverted) + flags |= mMask; + + record2.mFlags = flags; + + record.setModified (record2); + } + + virtual bool isEditable() const + { + return true; + } + }; + template struct WeightHeightColumn : public Column { @@ -767,8 +810,18 @@ namespace CSMWorld template struct ScriptColumn : public Column { - ScriptColumn() - : Column (Columns::ColumnId_ScriptText, ColumnBase::Display_Script, 0) {} + enum Type + { + Type_File, // regular script record + Type_Lines, // console context + Type_Info // dialogue context (not implemented yet) + }; + + ScriptColumn (Type type) + : Column (Columns::ColumnId_ScriptText, + type==Type_File ? ColumnBase::Display_Script : ColumnBase::Display_ScriptLines, + type==Type_File ? 0 : ColumnBase::Flag_Dialogue) + {} virtual QVariant get (const Record& record) const { @@ -1001,13 +1054,13 @@ namespace CSMWorld virtual QVariant get (const Record& record) const { - return record.get().mCharge; + return record.get().mChargeInt; } virtual void set (Record& record, const QVariant& data) { ESXRecordT record2 = record.get(); - record2.mCharge = data.toInt(); + record2.mChargeInt = data.toInt(); record.setModified (record2); } @@ -1225,36 +1278,6 @@ namespace CSMWorld } }; - template - struct ScopeColumn : public Column - { - ScopeColumn() - : Column (Columns::ColumnId_Scope, ColumnBase::Display_Integer, 0) - {} - - virtual QVariant get (const Record& record) const - { - return static_cast (record.get().mScope); - } - - virtual void set (Record& record, const QVariant& data) - { - ESXRecordT record2 = record.get(); - record2.mScope = static_cast (data.toInt()); - record.setModified (record2); - } - - virtual bool isEditable() const - { - return true; - } - - virtual bool isUserEditable() const - { - return false; - } - }; - template struct PosColumn : public Column @@ -1277,7 +1300,7 @@ namespace CSMWorld { ESXRecordT record2 = record.get(); - ESM::Position& position = record.get().*mPosition; + ESM::Position& position = record2.*mPosition; position.pos[mIndex] = data.toFloat(); @@ -1311,7 +1334,7 @@ namespace CSMWorld { ESXRecordT record2 = record.get(); - ESM::Position& position = record.get().*mPosition; + ESM::Position& position = record2.*mPosition; position.rot[mIndex] = data.toFloat(); @@ -1871,6 +1894,378 @@ namespace CSMWorld return true; } }; + + template + struct OwnerGlobalColumn : public Column + { + OwnerGlobalColumn() + : Column (Columns::ColumnId_OwnerGlobal, ColumnBase::Display_GlobalVariable) + {} + + virtual QVariant get (const Record& record) const + { + return QString::fromUtf8 (record.get().mGlobalVariable.c_str()); + } + + virtual void set (Record& record, const QVariant& data) + { + ESXRecordT record2 = record.get(); + + record2.mGlobalVariable = data.toString().toUtf8().constData(); + + record.setModified (record2); + } + + virtual bool isEditable() const + { + return true; + } + }; + + template + struct RefNumCounterColumn : public Column + { + RefNumCounterColumn() + : Column (Columns::ColumnId_RefNumCounter, ColumnBase::Display_Integer, 0) + {} + + virtual QVariant get (const Record& record) const + { + return static_cast (record.get().mRefNumCounter); + } + + virtual void set (Record& record, const QVariant& data) + { + ESXRecordT record2 = record.get(); + + record2.mRefNumCounter = data.toInt(); + + record.setModified (record2); + } + + virtual bool isEditable() const + { + return true; + } + + virtual bool isUserEditable() const + { + return false; + } + }; + + template + struct RefNumColumn : public Column + { + RefNumColumn() + : Column (Columns::ColumnId_RefNum, ColumnBase::Display_Integer, 0) + {} + + virtual QVariant get (const Record& record) const + { + return static_cast (record.get().mRefNum.mIndex); + } + + virtual void set (Record& record, const QVariant& data) + { + ESXRecordT record2 = record.get(); + + record2.mRefNum.mIndex = data.toInt(); + + record.setModified (record2); + } + + virtual bool isEditable() const + { + return true; + } + + virtual bool isUserEditable() const + { + return false; + } + }; + + template + struct SoundColumn : public Column + { + SoundColumn() + : Column (Columns::ColumnId_Sound, ColumnBase::Display_Sound) + {} + + virtual QVariant get (const Record& record) const + { + return QString::fromUtf8 (record.get().mSound.c_str()); + } + + virtual void set (Record& record, const QVariant& data) + { + ESXRecordT record2 = record.get(); + + record2.mSound = data.toString().toUtf8().constData(); + + record.setModified (record2); + } + + virtual bool isEditable() const + { + return true; + } + }; + + template + struct CreatureColumn : public Column + { + CreatureColumn() + : Column (Columns::ColumnId_Creature, ColumnBase::Display_Creature) + {} + + virtual QVariant get (const Record& record) const + { + return QString::fromUtf8 (record.get().mCreature.c_str()); + } + + virtual void set (Record& record, const QVariant& data) + { + ESXRecordT record2 = record.get(); + + record2.mCreature = data.toString().toUtf8().constData(); + + record.setModified (record2); + } + + virtual bool isEditable() const + { + return true; + } + }; + + template + struct SoundGeneratorTypeColumn : public Column + { + SoundGeneratorTypeColumn() + : Column (Columns::ColumnId_SoundGeneratorType, ColumnBase::Display_SoundGeneratorType) + {} + + virtual QVariant get (const Record& record) const + { + return static_cast (record.get().mType); + } + + virtual void set (Record& record, const QVariant& data) + { + ESXRecordT record2 = record.get(); + + record2.mType = data.toInt(); + + record.setModified (record2); + } + + virtual bool isEditable() const + { + return true; + } + }; + + template + struct BaseCostColumn : public Column + { + BaseCostColumn() : Column (Columns::ColumnId_BaseCost, ColumnBase::Display_Float) {} + + virtual QVariant get (const Record& record) const + { + return record.get().mData.mBaseCost; + } + + virtual void set (Record& record, const QVariant& data) + { + ESXRecordT record2 = record.get(); + record2.mData.mBaseCost = data.toFloat(); + record.setModified (record2); + } + + virtual bool isEditable() const + { + return true; + } + }; + + template + struct SchoolColumn : public Column + { + SchoolColumn() + : Column (Columns::ColumnId_School, ColumnBase::Display_School) + {} + + virtual QVariant get (const Record& record) const + { + return record.get().mData.mSchool; + } + + virtual void set (Record& record, const QVariant& data) + { + ESXRecordT record2 = record.get(); + + record2.mData.mSchool = data.toInt(); + + record.setModified (record2); + } + + virtual bool isEditable() const + { + return true; + } + }; + + template + struct EffectTextureColumn : public Column + { + EffectTextureColumn (Columns::ColumnId columnId) + : Column (columnId, ColumnBase::Display_Texture) + { + assert (this->mColumnId==Columns::ColumnId_Icon || + this->mColumnId==Columns::ColumnId_Particle); + } + + virtual QVariant get (const Record& record) const + { + return QString::fromUtf8 ( + (this->mColumnId==Columns::ColumnId_Icon ? + record.get().mIcon : record.get().mParticle).c_str()); + } + + virtual void set (Record& record, const QVariant& data) + { + ESXRecordT record2 = record.get(); + + (this->mColumnId==Columns::ColumnId_Icon ? + record2.mIcon : record2.mParticle) + = data.toString().toUtf8().constData(); + + record.setModified (record2); + } + + virtual bool isEditable() const + { + return true; + } + }; + + template + struct EffectObjectColumn : public Column + { + EffectObjectColumn (Columns::ColumnId columnId) + : Column (columnId, columnId==Columns::ColumnId_BoltObject ? ColumnBase::Display_Weapon : ColumnBase::Display_Static) + { + assert (this->mColumnId==Columns::ColumnId_CastingObject || + this->mColumnId==Columns::ColumnId_HitObject || + this->mColumnId==Columns::ColumnId_AreaObject || + this->mColumnId==Columns::ColumnId_BoltObject); + } + + virtual QVariant get (const Record& record) const + { + const std::string *string = 0; + + switch (this->mColumnId) + { + case Columns::ColumnId_CastingObject: string = &record.get().mCasting; break; + case Columns::ColumnId_HitObject: string = &record.get().mHit; break; + case Columns::ColumnId_AreaObject: string = &record.get().mArea; break; + case Columns::ColumnId_BoltObject: string = &record.get().mBolt; break; + } + + if (!string) + throw std::logic_error ("Unsupported column ID"); + + return QString::fromUtf8 (string->c_str()); + } + + virtual void set (Record& record, const QVariant& data) + { + std::string *string = 0; + + ESXRecordT record2 = record.get(); + + switch (this->mColumnId) + { + case Columns::ColumnId_CastingObject: string = &record2.mCasting; break; + case Columns::ColumnId_HitObject: string = &record2.mHit; break; + case Columns::ColumnId_AreaObject: string = &record2.mArea; break; + case Columns::ColumnId_BoltObject: string = &record2.mBolt; break; + } + + if (!string) + throw std::logic_error ("Unsupported column ID"); + + *string = data.toString().toUtf8().constData(); + + record.setModified (record2); + } + + virtual bool isEditable() const + { + return true; + } + }; + + template + struct EffectSoundColumn : public Column + { + EffectSoundColumn (Columns::ColumnId columnId) + : Column (columnId, ColumnBase::Display_Sound) + { + assert (this->mColumnId==Columns::ColumnId_CastingSound || + this->mColumnId==Columns::ColumnId_HitSound || + this->mColumnId==Columns::ColumnId_AreaSound || + this->mColumnId==Columns::ColumnId_BoltSound); + } + + virtual QVariant get (const Record& record) const + { + const std::string *string = 0; + + switch (this->mColumnId) + { + case Columns::ColumnId_CastingSound: string = &record.get().mCastSound; break; + case Columns::ColumnId_HitSound: string = &record.get().mHitSound; break; + case Columns::ColumnId_AreaSound: string = &record.get().mAreaSound; break; + case Columns::ColumnId_BoltSound: string = &record.get().mBoltSound; break; + } + + if (!string) + throw std::logic_error ("Unsupported column ID"); + + return QString::fromUtf8 (string->c_str()); + } + + virtual void set (Record& record, const QVariant& data) + { + std::string *string = 0; + + ESXRecordT record2 = record.get(); + + switch (this->mColumnId) + { + case Columns::ColumnId_CastingSound: string = &record2.mCastSound; break; + case Columns::ColumnId_HitSound: string = &record2.mHitSound; break; + case Columns::ColumnId_AreaSound: string = &record2.mAreaSound; break; + case Columns::ColumnId_BoltSound: string = &record2.mBoltSound; break; + } + + if (!string) + throw std::logic_error ("Unsupported column ID"); + + *string = data.toString().toUtf8().constData(); + + record.setModified (record2); + } + + virtual bool isEditable() const + { + return true; + } + }; } #endif diff --git a/apps/opencs/model/world/columns.cpp b/apps/opencs/model/world/columns.cpp index 0c44c2dcb..c8bf8ad64 100644 --- a/apps/opencs/model/world/columns.cpp +++ b/apps/opencs/model/world/columns.cpp @@ -173,7 +173,6 @@ namespace CSMWorld { ColumnId_Rank, "Rank" }, { ColumnId_Gender, "Gender" }, { ColumnId_PcRank, "PC Rank" }, - { ColumnId_Scope, "Scope" }, { ColumnId_ReferenceableId, "Referenceable ID" }, { ColumnId_NpcDestinations, "Destinations" }, { ColumnId_InventoryItemId, "ID"}, @@ -186,6 +185,27 @@ namespace CSMWorld { ColumnId_Vampire, "Vampire" }, { ColumnId_BodyPartType, "Bodypart Type" }, { ColumnId_MeshType, "Mesh Type" }, + { ColumnId_OwnerGlobal, "Owner Global" }, + { ColumnId_DefaultProfile, "Default Profile" }, + { ColumnId_BypassNewGame, "Bypass New Game" }, + { ColumnId_GlobalProfile, "Global Profile" }, + { ColumnId_RefNumCounter, "RefNum Counter" }, + { ColumnId_RefNum, "RefNum" }, + { ColumnId_Creature, "Creature" }, + { ColumnId_SoundGeneratorType, "Sound Generator Type" }, + { ColumnId_AllowSpellmaking, "Allow Spellmaking" }, + { ColumnId_AllowEnchanting, "Allow Enchanting" }, + { ColumnId_BaseCost, "Base Cost" }, + { ColumnId_School, "School" }, + { ColumnId_Particle, "Particle" }, + { ColumnId_CastingObject, "Casting Object" }, + { ColumnId_HitObject, "Hit Object" }, + { ColumnId_AreaObject, "Area Object" }, + { ColumnId_BoltObject, "Bolt Object" }, + { ColumnId_CastingSound, "Casting Sound" }, + { ColumnId_HitSound, "Hit Sound" }, + { ColumnId_AreaSound, "Area Sound" }, + { ColumnId_BoltSound, "Bolt Sound" }, { ColumnId_NpcDestinations, "Cell"}, { ColumnId_PosX, "X"}, @@ -340,6 +360,17 @@ namespace "Skin", "Clothing", "Armour", 0 }; + static const char *sSoundGeneratorType[] = + { + "Left Foot", "Right Foot", "Swim Left", "Swim Right", "Moan", "Roar", "Scream", + "Land", 0 + }; + + static const char *sSchools[] = + { + "Alteration", "Conjuration", "Destruction", "Illusion", "Mysticism", "Restoration", 0 + }; + const char **getEnumNames (CSMWorld::Columns::ColumnId column) { switch (column) @@ -361,6 +392,8 @@ namespace case CSMWorld::Columns::ColumnId_EnchantmentType: return sEnchantmentTypes; case CSMWorld::Columns::ColumnId_BodyPartType: return sBodyPartTypes; case CSMWorld::Columns::ColumnId_MeshType: return sMeshTypes; + case CSMWorld::Columns::ColumnId_SoundGeneratorType: return sSoundGeneratorType; + case CSMWorld::Columns::ColumnId_School: return sSchools; default: return 0; } diff --git a/apps/opencs/model/world/columns.hpp b/apps/opencs/model/world/columns.hpp index b06018dd4..69bbfa39f 100644 --- a/apps/opencs/model/world/columns.hpp +++ b/apps/opencs/model/world/columns.hpp @@ -167,7 +167,6 @@ namespace CSMWorld ColumnId_Rank = 152, ColumnId_Gender = 153, ColumnId_PcRank = 154, - ColumnId_Scope = 155, ColumnId_ReferenceableId = 156, ColumnId_ContainerContent = 157, ColumnId_ItemCount = 158, @@ -191,7 +190,27 @@ namespace CSMWorld ColumnId_RotZ = 176, ColumnId_DestinationCell = 177, ColumnId_Skill = 178, - + ColumnId_OwnerGlobal = 164, + ColumnId_DefaultProfile = 165, + ColumnId_BypassNewGame = 166, + ColumnId_GlobalProfile = 167, + ColumnId_RefNumCounter = 168, + ColumnId_RefNum = 169, + ColumnId_Creature = 170, + ColumnId_SoundGeneratorType = 171, + ColumnId_AllowSpellmaking = 172, + ColumnId_AllowEnchanting = 173, + ColumnId_BaseCost = 174, + ColumnId_School = 175, + ColumnId_Particle = 176, + ColumnId_CastingObject = 177, + ColumnId_HitObject = 178, + ColumnId_AreaObject = 179, + ColumnId_BoltObject = 180, + ColumnId_CastingSound = 177, + ColumnId_HitSound = 178, + ColumnId_AreaSound = 179, + ColumnId_BoltSound = 180, // Allocated to a separate value range, so we don't get a collision should we ever need // to extend the number of use values. ColumnId_UseValue1 = 0x10000, diff --git a/apps/opencs/model/world/commands.cpp b/apps/opencs/model/world/commands.cpp index 30c4bb892..959f6b345 100644 --- a/apps/opencs/model/world/commands.cpp +++ b/apps/opencs/model/world/commands.cpp @@ -10,13 +10,12 @@ CSMWorld::ModifyCommand::ModifyCommand (QAbstractItemModel& model, const QModelI const QVariant& new_, QUndoCommand* parent) : QUndoCommand (parent), mModel (model), mIndex (index), mNew (new_) { - mOld = mModel.data (mIndex, Qt::EditRole); - setText ("Modify " + mModel.headerData (mIndex.column(), Qt::Horizontal, Qt::DisplayRole).toString()); } void CSMWorld::ModifyCommand::redo() { + mOld = mModel.data (mIndex, Qt::EditRole); mModel.setData (mIndex, mNew); } @@ -25,6 +24,13 @@ void CSMWorld::ModifyCommand::undo() mModel.setData (mIndex, mOld); } + +void CSMWorld::CreateCommand::applyModifications() +{ + for (std::map::const_iterator iter (mValues.begin()); iter!=mValues.end(); ++iter) + mModel.setData (mModel.getModelIndex (mId, iter->first), iter->second); +} + CSMWorld::CreateCommand::CreateCommand (IdTable& model, const std::string& id, QUndoCommand* parent) : QUndoCommand (parent), mModel (model), mId (id), mType (UniversalId::Type_None) { @@ -44,9 +50,7 @@ void CSMWorld::CreateCommand::setType (UniversalId::Type type) void CSMWorld::CreateCommand::redo() { mModel.addRecord (mId, mType); - - for (std::map::const_iterator iter (mValues.begin()); iter!=mValues.end(); ++iter) - mModel.setData (mModel.getModelIndex (mId, iter->first), iter->second); + applyModifications(); } void CSMWorld::CreateCommand::undo() @@ -148,29 +152,24 @@ void CSMWorld::ReorderRowsCommand::undo() CSMWorld::CloneCommand::CloneCommand (CSMWorld::IdTable& model, const std::string& idOrigin, - const std::string& IdDestination, + const std::string& idDestination, const CSMWorld::UniversalId::Type type, - QUndoCommand* parent) : - QUndoCommand (parent), - mModel (model), - mIdOrigin (idOrigin), - mIdDestination (Misc::StringUtils::lowerCase (IdDestination)), - mType (type) + QUndoCommand* parent) +: CreateCommand (model, idDestination, parent), mIdOrigin (idOrigin) { - setText ( ("Clone record " + idOrigin + " to the " + IdDestination).c_str()); + setType (type); + setText ( ("Clone record " + idOrigin + " to the " + idDestination).c_str()); } void CSMWorld::CloneCommand::redo() { - mModel.cloneRecord (mIdOrigin, mIdDestination, mType); - - for (std::map::const_iterator iter (mValues.begin()); iter != mValues.end(); ++iter) - mModel.setData (mModel.getModelIndex (mIdDestination, iter->first), iter->second); + mModel.cloneRecord (mIdOrigin, mId, mType); + applyModifications(); } void CSMWorld::CloneCommand::undo() { - mModel.removeRow (mModel.getModelIndex (mIdDestination, 0).row()); + mModel.removeRow (mModel.getModelIndex (mId, 0).row()); } CSMWorld::DeleteNestedCommand::DeleteNestedCommand (IdTable& model, diff --git a/apps/opencs/model/world/commands.hpp b/apps/opencs/model/world/commands.hpp index d868ccf46..f486abab6 100644 --- a/apps/opencs/model/world/commands.hpp +++ b/apps/opencs/model/world/commands.hpp @@ -42,40 +42,44 @@ namespace CSMWorld virtual void undo(); }; - class CloneCommand : public QUndoCommand + class CreateCommand : public QUndoCommand { + std::map mValues; + + protected: + IdTable& mModel; - std::string mIdOrigin; - std::string mIdDestination; + std::string mId; UniversalId::Type mType; - std::map mValues; + + protected: + + /// Apply modifications set via addValue. + void applyModifications(); public: - CloneCommand (IdTable& model, const std::string& idOrigin, - const std::string& IdDestination, - const UniversalId::Type type, - QUndoCommand* parent = 0); + CreateCommand (IdTable& model, const std::string& id, QUndoCommand *parent = 0); + + void setType (UniversalId::Type type); + + void addValue (int column, const QVariant& value); virtual void redo(); virtual void undo(); }; - class CreateCommand : public QUndoCommand + class CloneCommand : public CreateCommand { - IdTable& mModel; - std::string mId; - UniversalId::Type mType; - std::map mValues; + std::string mIdOrigin; public: - CreateCommand (IdTable& model, const std::string& id, QUndoCommand *parent = 0); - - void setType (UniversalId::Type type); - - void addValue (int column, const QVariant& value); + CloneCommand (IdTable& model, const std::string& idOrigin, + const std::string& IdDestination, + const UniversalId::Type type, + QUndoCommand* parent = 0); virtual void redo(); diff --git a/apps/opencs/model/world/data.cpp b/apps/opencs/model/world/data.cpp index b9f6c6cf9..313518091 100644 --- a/apps/opencs/model/world/data.cpp +++ b/apps/opencs/model/world/data.cpp @@ -59,8 +59,8 @@ int CSMWorld::Data::count (RecordBase::State state, const CollectionBase& collec } CSMWorld::Data::Data (ToUTF8::FromType encoding, const ResourcesManager& resourcesManager) -: mEncoder (encoding), mRefs (mCells), mResourcesManager (resourcesManager), mReader (0), - mDialogue (0) +: mEncoder (encoding), mPathgrids (mCells), mRefs (mCells), + mResourcesManager (resourcesManager), mReader (0), mDialogue (0), mReaderIndex(0) { mGlobals.addColumn (new StringIdColumn); mGlobals.addColumn (new RecordStateColumn); @@ -71,7 +71,6 @@ CSMWorld::Data::Data (ToUTF8::FromType encoding, const ResourcesManager& resourc mGmsts.addColumn (new StringIdColumn); mGmsts.addColumn (new RecordStateColumn); mGmsts.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Gmst)); - mGmsts.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Gmst)); mGmsts.addColumn (new VarTypeColumn (ColumnBase::Display_GmstVarType)); mGmsts.addColumn (new VarValueColumn); @@ -131,7 +130,7 @@ CSMWorld::Data::Data (ToUTF8::FromType encoding, const ResourcesManager& resourc mScripts.addColumn (new StringIdColumn); mScripts.addColumn (new RecordStateColumn); mScripts.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Script)); - mScripts.addColumn (new ScriptColumn); + mScripts.addColumn (new ScriptColumn (ScriptColumn::Type_File)); mRegions.addColumn (new StringIdColumn); mRegions.addColumn (new RecordStateColumn); @@ -186,7 +185,7 @@ CSMWorld::Data::Data (ToUTF8::FromType encoding, const ResourcesManager& resourc mJournalInfos.addColumn (new StringIdColumn (true)); mJournalInfos.addColumn (new RecordStateColumn); - mJournalInfos.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Journal)); + mJournalInfos.addColumn (new FixedRecordTypeColumn (UniversalId::Type_JournalInfo)); mJournalInfos.addColumn (new TopicColumn (true)); mJournalInfos.addColumn (new QuestStatusTypeColumn); mJournalInfos.addColumn (new QuestIndexColumn); @@ -200,6 +199,7 @@ CSMWorld::Data::Data (ToUTF8::FromType encoding, const ResourcesManager& resourc mCells.addColumn (new FlagColumn (Columns::ColumnId_InteriorWater, ESM::Cell::HasWater)); mCells.addColumn (new FlagColumn (Columns::ColumnId_InteriorSky, ESM::Cell::QuasiEx)); mCells.addColumn (new RegionColumn); + mCells.addColumn (new RefNumCounterColumn); mEnchantments.addColumn (new StringIdColumn); mEnchantments.addColumn (new RecordStateColumn); @@ -220,6 +220,44 @@ CSMWorld::Data::Data (ToUTF8::FromType encoding, const ResourcesManager& resourc mBodyParts.addColumn (new ModelColumn); mBodyParts.addColumn (new RaceColumn); + mSoundGens.addColumn (new StringIdColumn); + mSoundGens.addColumn (new RecordStateColumn); + mSoundGens.addColumn (new FixedRecordTypeColumn (UniversalId::Type_SoundGen)); + mSoundGens.addColumn (new CreatureColumn); + mSoundGens.addColumn (new SoundColumn); + mSoundGens.addColumn (new SoundGeneratorTypeColumn); + + mMagicEffects.addColumn (new StringIdColumn); + mMagicEffects.addColumn (new RecordStateColumn); + mMagicEffects.addColumn (new FixedRecordTypeColumn (UniversalId::Type_MagicEffect)); + mMagicEffects.addColumn (new SchoolColumn); + mMagicEffects.addColumn (new BaseCostColumn); + mMagicEffects.addColumn (new EffectTextureColumn (Columns::ColumnId_Icon)); + mMagicEffects.addColumn (new EffectTextureColumn (Columns::ColumnId_Particle)); + mMagicEffects.addColumn (new EffectObjectColumn (Columns::ColumnId_CastingObject)); + mMagicEffects.addColumn (new EffectObjectColumn (Columns::ColumnId_HitObject)); + mMagicEffects.addColumn (new EffectObjectColumn (Columns::ColumnId_AreaObject)); + mMagicEffects.addColumn (new EffectObjectColumn (Columns::ColumnId_BoltObject)); + mMagicEffects.addColumn (new EffectSoundColumn (Columns::ColumnId_CastingSound)); + mMagicEffects.addColumn (new EffectSoundColumn (Columns::ColumnId_HitSound)); + mMagicEffects.addColumn (new EffectSoundColumn (Columns::ColumnId_AreaSound)); + mMagicEffects.addColumn (new EffectSoundColumn (Columns::ColumnId_BoltSound)); + mMagicEffects.addColumn (new FlagColumn ( + Columns::ColumnId_AllowSpellmaking, ESM::MagicEffect::AllowSpellmaking)); + mMagicEffects.addColumn (new FlagColumn ( + Columns::ColumnId_AllowEnchanting, ESM::MagicEffect::AllowEnchanting)); + mMagicEffects.addColumn (new FlagColumn ( + Columns::ColumnId_NegativeLight, ESM::MagicEffect::NegativeLight)); + mMagicEffects.addColumn (new DescriptionColumn); + + mPathgrids.addColumn (new StringIdColumn); + mPathgrids.addColumn (new RecordStateColumn); + mPathgrids.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Pathgrid)); + + mStartScripts.addColumn (new StringIdColumn); + mStartScripts.addColumn (new RecordStateColumn); + mStartScripts.addColumn (new FixedRecordTypeColumn (UniversalId::Type_StartScript)); + mRefs.addColumn (new StringIdColumn (true)); mRefs.addColumn (new RecordStateColumn); mRefs.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Reference)); @@ -250,13 +288,27 @@ CSMWorld::Data::Data (ToUTF8::FromType encoding, const ResourcesManager& resourc mRefs.addColumn (new LockLevelColumn); mRefs.addColumn (new KeyColumn); mRefs.addColumn (new TrapColumn); - - mFilters.addColumn (new StringIdColumn); - mFilters.addColumn (new RecordStateColumn); - mFilters.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Filter)); - mFilters.addColumn (new FilterColumn); - mFilters.addColumn (new DescriptionColumn); - mFilters.addColumn (new ScopeColumn); + mRefs.addColumn (new OwnerGlobalColumn); + mRefs.addColumn (new RefNumColumn); + + mFilters.addColumn (new StringIdColumn); + mFilters.addColumn (new RecordStateColumn); + mFilters.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Filter)); + mFilters.addColumn (new FilterColumn); + mFilters.addColumn (new DescriptionColumn); + + mDebugProfiles.addColumn (new StringIdColumn); + mDebugProfiles.addColumn (new RecordStateColumn); + mDebugProfiles.addColumn (new FixedRecordTypeColumn (UniversalId::Type_DebugProfile)); + mDebugProfiles.addColumn (new FlagColumn2 ( + Columns::ColumnId_DefaultProfile, ESM::DebugProfile::Flag_Default)); + mDebugProfiles.addColumn (new FlagColumn2 ( + Columns::ColumnId_BypassNewGame, ESM::DebugProfile::Flag_BypassNewGame)); + mDebugProfiles.addColumn (new FlagColumn2 ( + Columns::ColumnId_GlobalProfile, ESM::DebugProfile::Flag_Global)); + mDebugProfiles.addColumn (new DescriptionColumn); + mDebugProfiles.addColumn (new ScriptColumn ( + ScriptColumn::Type_Lines)); addModel (new IdTable (&mGlobals), UniversalId::Type_Global); addModel (new IdTable (&mGmsts), UniversalId::Type_Gmst); @@ -276,21 +328,26 @@ CSMWorld::Data::Data (ToUTF8::FromType encoding, const ResourcesManager& resourc addModel (new IdTable (&mCells, IdTable::Feature_ViewId), UniversalId::Type_Cell); addModel (new IdTable (&mEnchantments), UniversalId::Type_Enchantment); addModel (new IdTable (&mBodyParts), UniversalId::Type_BodyPart); + addModel (new IdTable (&mSoundGens), UniversalId::Type_SoundGen); + addModel (new IdTable (&mMagicEffects), UniversalId::Type_MagicEffect); + addModel (new IdTable (&mPathgrids), UniversalId::Type_Pathgrid); + addModel (new IdTable (&mStartScripts), UniversalId::Type_StartScript); addModel (new IdTable (&mReferenceables, IdTable::Feature_Preview), UniversalId::Type_Referenceable); addModel (new IdTable (&mRefs, IdTable::Feature_ViewCell | IdTable::Feature_Preview), UniversalId::Type_Reference); addModel (new IdTable (&mFilters), UniversalId::Type_Filter); - addModel (new ResourceTable (&mResourcesManager.get (UniversalId::Type_Mesh)), + addModel (new IdTable (&mDebugProfiles), UniversalId::Type_DebugProfile); + addModel (new ResourceTable (&mResourcesManager.get (UniversalId::Type_Meshes)), UniversalId::Type_Mesh); - addModel (new ResourceTable (&mResourcesManager.get (UniversalId::Type_Icon)), + addModel (new ResourceTable (&mResourcesManager.get (UniversalId::Type_Icons)), UniversalId::Type_Icon); - addModel (new ResourceTable (&mResourcesManager.get (UniversalId::Type_Music)), + addModel (new ResourceTable (&mResourcesManager.get (UniversalId::Type_Musics)), UniversalId::Type_Music); - addModel (new ResourceTable (&mResourcesManager.get (UniversalId::Type_SoundRes)), + addModel (new ResourceTable (&mResourcesManager.get (UniversalId::Type_SoundsRes)), UniversalId::Type_SoundRes); - addModel (new ResourceTable (&mResourcesManager.get (UniversalId::Type_Texture)), + addModel (new ResourceTable (&mResourcesManager.get (UniversalId::Type_Textures)), UniversalId::Type_Texture); - addModel (new ResourceTable (&mResourcesManager.get (UniversalId::Type_Video)), + addModel (new ResourceTable (&mResourcesManager.get (UniversalId::Type_Videos)), UniversalId::Type_Video); } @@ -483,12 +540,12 @@ CSMWorld::RefCollection& CSMWorld::Data::getReferences() return mRefs; } -const CSMWorld::IdCollection& CSMWorld::Data::getFilters() const +const CSMWorld::IdCollection& CSMWorld::Data::getFilters() const { return mFilters; } -CSMWorld::IdCollection& CSMWorld::Data::getFilters() +CSMWorld::IdCollection& CSMWorld::Data::getFilters() { return mFilters; } @@ -513,9 +570,69 @@ CSMWorld::IdCollection& CSMWorld::Data::getBodyParts() return mBodyParts; } +const CSMWorld::IdCollection& CSMWorld::Data::getDebugProfiles() const +{ + return mDebugProfiles; +} + +CSMWorld::IdCollection& CSMWorld::Data::getDebugProfiles() +{ + return mDebugProfiles; +} + +const CSMWorld::IdCollection& CSMWorld::Data::getLand() const +{ + return mLand; +} + +const CSMWorld::IdCollection& CSMWorld::Data::getLandTextures() const +{ + return mLandTextures; +} + +const CSMWorld::IdCollection& CSMWorld::Data::getSoundGens() const +{ + return mSoundGens; +} + +CSMWorld::IdCollection& CSMWorld::Data::getSoundGens() +{ + return mSoundGens; +} + +const CSMWorld::IdCollection& CSMWorld::Data::getMagicEffects() const +{ + return mMagicEffects; +} + +CSMWorld::IdCollection& CSMWorld::Data::getMagicEffects() +{ + return mMagicEffects; +} + +const CSMWorld::SubCellCollection& CSMWorld::Data::getPathgrids() const +{ + return mPathgrids; +} + +CSMWorld::SubCellCollection& CSMWorld::Data::getPathgrids() +{ + return mPathgrids; +} + +const CSMWorld::IdCollection& CSMWorld::Data::getStartScripts() const +{ + return mStartScripts; +} + +CSMWorld::IdCollection& CSMWorld::Data::getStartScripts() +{ + return mStartScripts; +} + const CSMWorld::Resources& CSMWorld::Data::getResources (const UniversalId& id) const { - return mResourcesManager.get (UniversalId::getParentType (id.getType())); + return mResourcesManager.get (id.getType()); } QAbstractItemModel *CSMWorld::Data::getTableModel (const CSMWorld::UniversalId& id) @@ -547,13 +664,17 @@ void CSMWorld::Data::merge() int CSMWorld::Data::startLoading (const boost::filesystem::path& path, bool base, bool project) { - delete mReader; + // Don't delete the Reader yet. Some record types store a reference to the Reader to handle on-demand loading + boost::shared_ptr ptr(mReader); + mReaders.push_back(ptr); mReader = 0; + mDialogue = 0; mRefLoadCache.clear(); mReader = new ESM::ESMReader; mReader->setEncoder (&mEncoder); + mReader->setIndex(mReaderIndex++); mReader->open (path.string()); mBase = base; @@ -565,15 +686,26 @@ int CSMWorld::Data::startLoading (const boost::filesystem::path& path, bool base return mReader->getRecordCount(); } -bool CSMWorld::Data::continueLoading (CSMDoc::Stage::Messages& messages) +bool CSMWorld::Data::continueLoading (CSMDoc::Messages& messages) { if (!mReader) throw std::logic_error ("can't continue loading, because no load has been started"); if (!mReader->hasMoreRecs()) { - delete mReader; + if (mBase) + { + // Don't delete the Reader yet. Some record types store a reference to the Reader to handle on-demand loading. + // We don't store non-base reader, because everything going into modified will be + // fully loaded during the initial loading process. + boost::shared_ptr ptr(mReader); + mReaders.push_back(ptr); + } + else + delete mReader; + mReader = 0; + mDialogue = 0; mRefLoadCache.clear(); return true; @@ -582,6 +714,8 @@ bool CSMWorld::Data::continueLoading (CSMDoc::Stage::Messages& messages) ESM::NAME n = mReader->getRecName(); mReader->getRecHeader(); + bool unhandledRecord = false; + switch (n.val) { case ESM::REC_GLOB: mGlobals.load (*mReader, mBase); break; @@ -597,6 +731,24 @@ bool CSMWorld::Data::continueLoading (CSMDoc::Stage::Messages& messages) case ESM::REC_SPEL: mSpells.load (*mReader, mBase); break; case ESM::REC_ENCH: mEnchantments.load (*mReader, mBase); break; case ESM::REC_BODY: mBodyParts.load (*mReader, mBase); break; + case ESM::REC_SNDG: mSoundGens.load (*mReader, mBase); break; + case ESM::REC_MGEF: mMagicEffects.load (*mReader, mBase); break; + case ESM::REC_PGRD: mPathgrids.load (*mReader, mBase); break; + case ESM::REC_SSCR: mStartScripts.load (*mReader, mBase); break; + + case ESM::REC_LTEX: mLandTextures.load (*mReader, mBase); break; + + case ESM::REC_LAND: + { + int index = mLand.load(*mReader, mBase); + + if (index!=-1 && !mBase) + mLand.getRecord (index).mModified.mLand->loadData ( + ESM::Land::DATA_VHGT | ESM::Land::DATA_VNML | ESM::Land::DATA_VCLR | + ESM::Land::DATA_VTEX); + + break; + } case ESM::REC_CELL: { @@ -658,8 +810,8 @@ bool CSMWorld::Data::continueLoading (CSMDoc::Stage::Messages& messages) } else { - messages.push_back (std::make_pair (UniversalId::Type_None, - "Trying to delete dialogue record " + id + " which does not exist")); + messages.add (UniversalId::Type_None, + "Trying to delete dialogue record " + id + " which does not exist"); } } else @@ -675,8 +827,8 @@ bool CSMWorld::Data::continueLoading (CSMDoc::Stage::Messages& messages) { if (!mDialogue) { - messages.push_back (std::make_pair (UniversalId::Type_None, - "Found info record not following a dialogue record")); + messages.add (UniversalId::Type_None, + "Found info record not following a dialogue record"); mReader->skipRecord(); break; @@ -692,23 +844,36 @@ bool CSMWorld::Data::continueLoading (CSMDoc::Stage::Messages& messages) case ESM::REC_FILT: - if (mProject) + if (!mProject) + { + unhandledRecord = true; + break; + } + + mFilters.load (*mReader, mBase); + break; + + case ESM::REC_DBGP: + + if (!mProject) { - mFilters.load (*mReader, mBase); - mFilters.setData (mFilters.getSize()-1, - mFilters.findColumnIndex (CSMWorld::Columns::ColumnId_Scope), - static_cast (CSMFilter::Filter::Scope_Project)); + unhandledRecord = true; break; } - // fall through (filter record in a content file is an error with format 0) + mDebugProfiles.load (*mReader, mBase); + break; default: - messages.push_back (std::make_pair (UniversalId::Type_None, - "Unsupported record type: " + n.toString())); + unhandledRecord = true; + } + + if (unhandledRecord) + { + messages.add (UniversalId::Type_None, "Unsupported record type: " + n.toString()); - mReader->skipRecord(); + mReader->skipRecord(); } return false; @@ -733,6 +898,8 @@ bool CSMWorld::Data::hasId (const std::string& id) const getCells().searchId (id)!=-1 || getEnchantments().searchId (id)!=-1 || getBodyParts().searchId (id)!=-1 || + getSoundGens().searchId (id)!=-1 || + getMagicEffects().searchId (id)!=-1 || getReferenceables().searchId (id)!=-1; } @@ -753,7 +920,12 @@ int CSMWorld::Data::count (RecordBase::State state) const count (state, mCells) + count (state, mEnchantments) + count (state, mBodyParts) + - count (state, mReferenceables); + count (state, mLand) + + count (state, mLandTextures) + + count (state, mSoundGens) + + count (state, mMagicEffects) + + count (state, mReferenceables) + + count (state, mPathgrids); } void CSMWorld::Data::setDescription (const std::string& description) @@ -795,6 +967,8 @@ std::vector CSMWorld::Data::getIds (bool listDeleted) const appendIds (ids, mCells, listDeleted); appendIds (ids, mEnchantments, listDeleted); appendIds (ids, mBodyParts, listDeleted); + appendIds (ids, mSoundGens, listDeleted); + appendIds (ids, mMagicEffects, listDeleted); appendIds (ids, mReferenceables, listDeleted); std::sort (ids.begin(), ids.end()); diff --git a/apps/opencs/model/world/data.hpp b/apps/opencs/model/world/data.hpp index cbf13d8b1..298a9be1f 100644 --- a/apps/opencs/model/world/data.hpp +++ b/apps/opencs/model/world/data.hpp @@ -23,19 +23,26 @@ #include #include #include +#include +#include +#include +#include +#include #include -#include "../filter/filter.hpp" - #include "../doc/stage.hpp" #include "idcollection.hpp" #include "universalid.hpp" #include "cell.hpp" +#include "land.hpp" +#include "landtexture.hpp" #include "refidcollection.hpp" #include "refcollection.hpp" #include "infocollection.hpp" +#include "pathgrid.hpp" +#include "subcellcollection.hpp" class QAbstractItemModel; @@ -70,12 +77,19 @@ namespace CSMWorld IdCollection mJournals; IdCollection mEnchantments; IdCollection mBodyParts; + IdCollection mMagicEffects; + SubCellCollection mPathgrids; + IdCollection mDebugProfiles; + IdCollection mSoundGens; + IdCollection mStartScripts; InfoCollection mTopicInfos; InfoCollection mJournalInfos; IdCollection mCells; + IdCollection mLandTextures; + IdCollection mLand; RefIdCollection mReferenceables; RefCollection mRefs; - IdCollection mFilters; + IdCollection mFilters; const ResourcesManager& mResourcesManager; std::vector mModels; std::map mModelIndex; @@ -86,6 +100,9 @@ namespace CSMWorld bool mBase; bool mProject; std::map > mRefLoadCache; + int mReaderIndex; + + std::vector > mReaders; // not implemented Data (const Data&); @@ -178,9 +195,9 @@ namespace CSMWorld RefCollection& getReferences(); - const IdCollection& getFilters() const; + const IdCollection& getFilters() const; - IdCollection& getFilters(); + IdCollection& getFilters(); const IdCollection& getEnchantments() const; @@ -190,6 +207,30 @@ namespace CSMWorld IdCollection& getBodyParts(); + const IdCollection& getDebugProfiles() const; + + IdCollection& getDebugProfiles(); + + const IdCollection& getLand() const; + + const IdCollection& getLandTextures() const; + + const IdCollection& getSoundGens() const; + + IdCollection& getSoundGens(); + + const IdCollection& getMagicEffects() const; + + IdCollection& getMagicEffects(); + + const SubCellCollection& getPathgrids() const; + + SubCellCollection& getPathgrids(); + + const IdCollection& getStartScripts() const; + + IdCollection& getStartScripts(); + /// Throws an exception, if \a id does not match a resources list. const Resources& getResources (const UniversalId& id) const; @@ -209,7 +250,7 @@ namespace CSMWorld /// ///< \return estimated number of records - bool continueLoading (CSMDoc::Stage::Messages& messages); + bool continueLoading (CSMDoc::Messages& messages); ///< \return Finished? bool hasId (const std::string& id) const; @@ -242,4 +283,4 @@ namespace CSMWorld }; } -#endif \ No newline at end of file +#endif diff --git a/apps/opencs/model/world/idcollection.hpp b/apps/opencs/model/world/idcollection.hpp index 7e7756ff3..f00ea447a 100644 --- a/apps/opencs/model/world/idcollection.hpp +++ b/apps/opencs/model/world/idcollection.hpp @@ -11,14 +11,19 @@ namespace CSMWorld template > class IdCollection : public Collection { + virtual void loadRecord (ESXRecordT& record, ESM::ESMReader& reader); + public: - void load (ESM::ESMReader& reader, bool base); + /// \return Index of loaded record (-1 if no record was loaded) + int load (ESM::ESMReader& reader, bool base); /// \param index Index at which the record can be found. /// Special values: -2 index unknown, -1 record does not exist yet and therefore /// does not have an index - void load (const ESXRecordT& record, bool base, int index = -2); + /// + /// \return index + int load (const ESXRecordT& record, bool base, int index = -2); bool tryDelete (const std::string& id); ///< Try deleting \a id. If the id does not exist or can't be deleted the call is ignored. @@ -27,7 +32,14 @@ namespace CSMWorld }; template - void IdCollection::load (ESM::ESMReader& reader, bool base) + void IdCollection::loadRecord (ESXRecordT& record, + ESM::ESMReader& reader) + { + record.load (reader); + } + + template + int IdCollection::load (ESM::ESMReader& reader, bool base) { std::string id = reader.getHNOString ("NAME"); @@ -55,6 +67,8 @@ namespace CSMWorld record.mState = RecordBase::State_Deleted; this->setRecord (index, record); } + + return -1; } else { @@ -69,14 +83,22 @@ namespace CSMWorld record = this->getRecord (index).get(); } - record.load (reader); + loadRecord (record, reader); + + if (index==-1) + { + std::string newId = IdAccessorT().getId(record); + int newIndex = this->searchId(newId); + if (newIndex != -1 && id != newId) + index = newIndex; + } - load (record, base, index); + return load (record, base, index); } } template - void IdCollection::load (const ESXRecordT& record, bool base, + int IdCollection::load (const ESXRecordT& record, bool base, int index) { if (index==-2) @@ -89,6 +111,7 @@ namespace CSMWorld record2.mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly; (base ? record2.mBase : record2.mModified) = record; + index = this->getSize(); this->appendRecord (record2); } else @@ -103,6 +126,8 @@ namespace CSMWorld this->setRecord (index, record2); } + + return index; } template diff --git a/apps/opencs/model/world/idtable.cpp b/apps/opencs/model/world/idtable.cpp index 410e0d3f0..2852cb581 100644 --- a/apps/opencs/model/world/idtable.cpp +++ b/apps/opencs/model/world/idtable.cpp @@ -37,7 +37,7 @@ int CSMWorld::IdTable::columnCount (const QModelIndex & parent) const QVariant CSMWorld::IdTable::data (const QModelIndex & index, int role) const { - if (role!=Qt::DisplayRole && role!=Qt::EditRole) + if ((role!=Qt::DisplayRole && role!=Qt::EditRole) || index.row() < 0 || index.column() < 0) return QVariant(); if (role==Qt::EditRole && !mIdCollection->getColumn (index.column()).isEditable()) diff --git a/apps/opencs/model/world/idtableproxymodel.cpp b/apps/opencs/model/world/idtableproxymodel.cpp index a5e9d354a..93c1749c6 100644 --- a/apps/opencs/model/world/idtableproxymodel.cpp +++ b/apps/opencs/model/world/idtableproxymodel.cpp @@ -47,4 +47,9 @@ void CSMWorld::IdTableProxyModel::setFilter (const boost::shared_ptr& filter); + + protected: + + bool lessThan(const QModelIndex &left, const QModelIndex &right) const; }; } -#endif \ No newline at end of file +#endif diff --git a/apps/opencs/model/world/land.cpp b/apps/opencs/model/world/land.cpp new file mode 100644 index 000000000..119e18761 --- /dev/null +++ b/apps/opencs/model/world/land.cpp @@ -0,0 +1,28 @@ +#include "land.hpp" + +#include + +namespace CSMWorld +{ + + Land::Land() + { + mLand.reset(new ESM::Land()); + } + + void Land::load(ESM::ESMReader &esm) + { + mLand->load(esm); + + std::ostringstream stream; + stream << "#" << mLand->mX << " " << mLand->mY; + + mId = stream.str(); + } + + void Land::blank() + { + /// \todo + } + +} diff --git a/apps/opencs/model/world/land.hpp b/apps/opencs/model/world/land.hpp new file mode 100644 index 000000000..e97a2d7dd --- /dev/null +++ b/apps/opencs/model/world/land.hpp @@ -0,0 +1,29 @@ +#ifndef CSM_WORLD_LAND_H +#define CSM_WORLD_LAND_H + +#include +#include +#include + +namespace CSMWorld +{ + /// \brief Wrapper for Land record. Encodes X and Y cell index in the ID. + /// + /// \todo Add worldspace support to the Land record. + /// \todo Add a proper copy constructor (currently worked around using shared_ptr) + struct Land + { + Land(); + + boost::shared_ptr mLand; + + std::string mId; + + /// Loads the metadata and ID + void load (ESM::ESMReader &esm); + + void blank(); + }; +} + +#endif diff --git a/apps/opencs/model/world/landtexture.cpp b/apps/opencs/model/world/landtexture.cpp new file mode 100644 index 000000000..4725866a5 --- /dev/null +++ b/apps/opencs/model/world/landtexture.cpp @@ -0,0 +1,21 @@ +#include "landtexture.hpp" + +#include + +namespace CSMWorld +{ + + void LandTexture::load(ESM::ESMReader &esm) + { + ESM::LandTexture::load(esm); + + int plugin = esm.getIndex(); + + std::ostringstream stream; + + stream << mIndex << "_" << plugin; + + mId = stream.str(); + } + +} diff --git a/apps/opencs/model/world/landtexture.hpp b/apps/opencs/model/world/landtexture.hpp new file mode 100644 index 000000000..b13903186 --- /dev/null +++ b/apps/opencs/model/world/landtexture.hpp @@ -0,0 +1,22 @@ +#ifndef CSM_WORLD_LANDTEXTURE_H +#define CSM_WORLD_LANDTEXTURE_H + +#include + +#include + +namespace CSMWorld +{ + /// \brief Wrapper for LandTexture record. Encodes mIndex and the plugin index (obtained from ESMReader) + /// in the ID. + /// + /// \attention The mId field of the ESM::LandTexture struct is not used. + struct LandTexture : public ESM::LandTexture + { + std::string mId; + + void load (ESM::ESMReader &esm); + }; +} + +#endif diff --git a/apps/opencs/model/world/pathgrid.cpp b/apps/opencs/model/world/pathgrid.cpp new file mode 100644 index 000000000..5c66e7d8e --- /dev/null +++ b/apps/opencs/model/world/pathgrid.cpp @@ -0,0 +1,36 @@ +#include "cell.hpp" +#include "idcollection.hpp" +#include "pathgrid.hpp" + +#include + +void CSMWorld::Pathgrid::load (ESM::ESMReader &esm, const IdCollection& cells) +{ + load (esm); + + // correct ID + if (!mId.empty() && mId[0]!='#' && cells.searchId (mId)==-1) + { + std::ostringstream stream; + + stream << "#" << mData.mX << " " << mData.mY; + + mId = stream.str(); + } +} + +void CSMWorld::Pathgrid::load (ESM::ESMReader &esm) +{ + ESM::Pathgrid::load (esm); + + if (mCell.empty()) + { + std::ostringstream stream; + + stream << "#" << mData.mX << " " << mData.mY; + + mId = stream.str(); + } + else + mId = mCell; +} diff --git a/apps/opencs/model/world/pathgrid.hpp b/apps/opencs/model/world/pathgrid.hpp new file mode 100644 index 000000000..d8cc89f24 --- /dev/null +++ b/apps/opencs/model/world/pathgrid.hpp @@ -0,0 +1,29 @@ +#ifndef CSM_WOLRD_PATHGRID_H +#define CSM_WOLRD_PATHGRID_H + +#include +#include + +#include + +namespace CSMWorld +{ + struct Cell; + template + class IdCollection; + + /// \brief Wrapper for Pathgrid record + /// + /// \attention The mData.mX and mData.mY fields of the ESM::Pathgrid struct are not used. + /// Exterior cell coordinates are encoded in the pathgrid ID. + struct Pathgrid : public ESM::Pathgrid + { + std::string mId; + + void load (ESM::ESMReader &esm, const IdCollection& cells); + + void load (ESM::ESMReader &esm); + }; +} + +#endif \ No newline at end of file diff --git a/apps/opencs/model/world/ref.cpp b/apps/opencs/model/world/ref.cpp index d1bd771f8..f3c1b0b73 100644 --- a/apps/opencs/model/world/ref.cpp +++ b/apps/opencs/model/world/ref.cpp @@ -4,7 +4,5 @@ CSMWorld::CellRef::CellRef() { mRefNum.mIndex = 0; - - // special marker: This reference does not have a RefNum assign to it yet. - mRefNum.mContentFile = -2; + mRefNum.mContentFile = 0; } \ No newline at end of file diff --git a/apps/opencs/model/world/refcollection.cpp b/apps/opencs/model/world/refcollection.cpp index c516e2c3e..47f0276c6 100644 --- a/apps/opencs/model/world/refcollection.cpp +++ b/apps/opencs/model/world/refcollection.cpp @@ -11,7 +11,7 @@ #include "record.hpp" void CSMWorld::RefCollection::load (ESM::ESMReader& reader, int cellIndex, bool base, - std::map& cache, CSMDoc::Stage::Messages& messages) + std::map& cache, CSMDoc::Messages& messages) { Record cell = mCells.getRecord (cellIndex); @@ -36,8 +36,7 @@ void CSMWorld::RefCollection::load (ESM::ESMReader& reader, int cellIndex, bool CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Cell, mCells.getId (cellIndex)); - messages.push_back (std::make_pair (id, - "Attempt to delete a non-existing reference")); + messages.add (id, "Attempt to delete a non-existing reference"); continue; } diff --git a/apps/opencs/model/world/refcollection.hpp b/apps/opencs/model/world/refcollection.hpp index 63d369ed9..4ecc32b2f 100644 --- a/apps/opencs/model/world/refcollection.hpp +++ b/apps/opencs/model/world/refcollection.hpp @@ -28,7 +28,7 @@ namespace CSMWorld void load (ESM::ESMReader& reader, int cellIndex, bool base, std::map& cache, - CSMDoc::Stage::Messages& messages); + CSMDoc::Messages& messages); ///< Load a sequence of references. std::string getNewId(); diff --git a/apps/opencs/model/world/refidadapterimp.cpp b/apps/opencs/model/world/refidadapterimp.cpp index ac546db51..99117cc7f 100644 --- a/apps/opencs/model/world/refidadapterimp.cpp +++ b/apps/opencs/model/world/refidadapterimp.cpp @@ -240,7 +240,14 @@ void CSMWorld::ContainerRefIdAdapter::setData (const RefIdColumn *column, RefIdD } CSMWorld::CreatureColumns::CreatureColumns (const ActorColumns& actorColumns) -: ActorColumns (actorColumns) +: ActorColumns (actorColumns), + mType(NULL), + mSoul(NULL), + mScale(NULL), + mOriginal(NULL), + mCombat(NULL), + mMagic(NULL), + mStealth(NULL) {} CSMWorld::CreatureRefIdAdapter::CreatureRefIdAdapter (const CreatureColumns& columns) @@ -448,7 +455,14 @@ void CSMWorld::MiscRefIdAdapter::setData (const RefIdColumn *column, RefIdData& InventoryRefIdAdapter::setData (column, data, index, value); } -CSMWorld::NpcColumns::NpcColumns (const ActorColumns& actorColumns) : ActorColumns (actorColumns) {} +CSMWorld::NpcColumns::NpcColumns (const ActorColumns& actorColumns) +: ActorColumns (actorColumns), + mRace(NULL), + mClass(NULL), + mFaction(NULL), + mHair(NULL), + mHead(NULL) +{} CSMWorld::NpcRefIdAdapter::NpcRefIdAdapter (const NpcColumns& columns) : ActorRefIdAdapter (UniversalId::Type_Npc, columns), mColumns (columns) diff --git a/apps/opencs/model/world/resources.cpp b/apps/opencs/model/world/resources.cpp index 8e255bc96..13c8df84d 100644 --- a/apps/opencs/model/world/resources.cpp +++ b/apps/opencs/model/world/resources.cpp @@ -3,6 +3,7 @@ #include #include +#include #include @@ -55,7 +56,9 @@ CSMWorld::Resources::Resources (const std::string& baseDirectory, UniversalId::T std::string file = iter->substr (baseSize+1); mFiles.push_back (file); - mIndex.insert (std::make_pair (file, static_cast (mFiles.size())-1)); + std::replace (file.begin(), file.end(), '\\', '/'); + mIndex.insert (std::make_pair ( + Misc::StringUtils::lowerCase (file), static_cast (mFiles.size())-1)); } } } @@ -89,6 +92,8 @@ int CSMWorld::Resources::searchId (const std::string& id) const { std::string id2 = Misc::StringUtils::lowerCase (id); + std::replace (id2.begin(), id2.end(), '\\', '/'); + std::map::const_iterator iter = mIndex.find (id2); if (iter==mIndex.end()) diff --git a/apps/opencs/model/world/resourcesmanager.cpp b/apps/opencs/model/world/resourcesmanager.cpp index 5692d30ac..50014f4b5 100644 --- a/apps/opencs/model/world/resourcesmanager.cpp +++ b/apps/opencs/model/world/resourcesmanager.cpp @@ -6,6 +6,8 @@ void CSMWorld::ResourcesManager::addResources (const Resources& resources) { mResources.insert (std::make_pair (resources.getType(), resources)); + mResources.insert (std::make_pair (UniversalId::getParentType (resources.getType()), + resources)); } void CSMWorld::ResourcesManager::listResources() diff --git a/apps/opencs/model/world/resourcetable.cpp b/apps/opencs/model/world/resourcetable.cpp index 86de0a6a6..a7180434a 100644 --- a/apps/opencs/model/world/resourcetable.cpp +++ b/apps/opencs/model/world/resourcetable.cpp @@ -50,7 +50,7 @@ QVariant CSMWorld::ResourceTable::headerData (int section, Qt::Orientation orien return QVariant(); if (role==ColumnBase::Role_Flags) - return ColumnBase::Flag_Table; + return section==0 ? ColumnBase::Flag_Table : 0; switch (section) { @@ -86,7 +86,7 @@ bool CSMWorld::ResourceTable::setData ( const QModelIndex &index, const QVariant Qt::ItemFlags CSMWorld::ResourceTable::flags (const QModelIndex & index) const { - return Qt::ItemIsSelectable | Qt::ItemIsEnabled;; + return Qt::ItemIsSelectable | Qt::ItemIsEnabled; } QModelIndex CSMWorld::ResourceTable::index (int row, int column, const QModelIndex& parent) diff --git a/apps/opencs/model/world/scope.cpp b/apps/opencs/model/world/scope.cpp new file mode 100644 index 000000000..e3ebf5ebd --- /dev/null +++ b/apps/opencs/model/world/scope.cpp @@ -0,0 +1,25 @@ + +#include "scope.hpp" + +#include + +#include + +CSMWorld::Scope CSMWorld::getScopeFromId (const std::string& id) +{ + // get root namespace + std::string namespace_; + + std::string::size_type i = id.find ("::"); + + if (i!=std::string::npos) + namespace_ = Misc::StringUtils::lowerCase (id.substr (0, i)); + + if (namespace_=="project") + return Scope_Project; + + if (namespace_=="session") + return Scope_Session; + + return Scope_Content; +} \ No newline at end of file diff --git a/apps/opencs/model/world/scope.hpp b/apps/opencs/model/world/scope.hpp new file mode 100644 index 000000000..3983d91f5 --- /dev/null +++ b/apps/opencs/model/world/scope.hpp @@ -0,0 +1,23 @@ +#ifndef CSM_WOLRD_SCOPE_H +#define CSM_WOLRD_SCOPE_H + +#include + +namespace CSMWorld +{ + enum Scope + { + // record stored in content file + Scope_Content = 1, + + // record stored in project file + Scope_Project = 2, + + // record that exists only for the duration of one editing session + Scope_Session = 4 + }; + + Scope getScopeFromId (const std::string& id); +} + +#endif diff --git a/apps/opencs/model/world/scriptcontext.cpp b/apps/opencs/model/world/scriptcontext.cpp index 9da49defe..0d2b9984e 100644 --- a/apps/opencs/model/world/scriptcontext.cpp +++ b/apps/opencs/model/world/scriptcontext.cpp @@ -47,7 +47,7 @@ std::pair CSMWorld::ScriptContext::getMemberType (const std::string& int index = mData.getScripts().searchId (id2); bool reference = false; - if (index!=-1) + if (index==-1) { // ID is not a script ID. Search for a matching referenceable instead. index = mData.getReferenceables().searchId (id2); @@ -55,19 +55,16 @@ std::pair CSMWorld::ScriptContext::getMemberType (const std::string& if (index!=-1) { // Referenceable found. - int columnIndex = mData.getReferenceables().searchColumnIndex (Columns::ColumnId_Script); + int columnIndex = mData.getReferenceables().findColumnIndex (Columns::ColumnId_Script); + + id2 = Misc::StringUtils::lowerCase (mData.getReferenceables(). + getData (index, columnIndex).toString().toUtf8().constData()); - if (columnIndex!=-1) + if (!id2.empty()) { - id2 = Misc::StringUtils::lowerCase (mData.getReferenceables(). - getData (index, columnIndex).toString().toUtf8().constData()); - - if (!id2.empty()) - { - // Referenceable has a script -> use it. - index = mData.getScripts().searchId (id2); - reference = true; - } + // Referenceable has a script -> use it. + index = mData.getScripts().searchId (id2); + reference = true; } } } @@ -99,7 +96,7 @@ bool CSMWorld::ScriptContext::isId (const std::string& name) const { mIds = mData.getIds(); - std::for_each (mIds.begin(), mIds.end(), &Misc::StringUtils::lowerCase); + std::for_each (mIds.begin(), mIds.end(), &Misc::StringUtils::toLower); std::sort (mIds.begin(), mIds.end()); mIdsUpdated = true; diff --git a/apps/opencs/model/world/subcellcollection.hpp b/apps/opencs/model/world/subcellcollection.hpp new file mode 100644 index 000000000..74bb6c955 --- /dev/null +++ b/apps/opencs/model/world/subcellcollection.hpp @@ -0,0 +1,45 @@ +#ifndef CSM_WOLRD_SUBCOLLECTION_H +#define CSM_WOLRD_SUBCOLLECTION_H + +namespace ESM +{ + class ESMReader; +} + +namespace CSMWorld +{ + struct Cell; + template + class IdCollection; + + /// \brief Single type collection of top level records that are associated with cells + template > + class SubCellCollection : public IdCollection + { + const IdCollection& mCells; + + virtual void loadRecord (ESXRecordT& record, ESM::ESMReader& reader); + + public: + + SubCellCollection (const IdCollection& cells); + + + }; + + template + void SubCellCollection::loadRecord (ESXRecordT& record, + ESM::ESMReader& reader) + { + record.load (reader, mCells); + } + + template + SubCellCollection::SubCellCollection ( + const IdCollection& cells) + : mCells (cells) + {} + +} + +#endif diff --git a/apps/opencs/model/world/tablemimedata.cpp b/apps/opencs/model/world/tablemimedata.cpp index 9b80650ab..d40e0c217 100644 --- a/apps/opencs/model/world/tablemimedata.cpp +++ b/apps/opencs/model/world/tablemimedata.cpp @@ -91,7 +91,7 @@ return ( type == CSMWorld::ColumnBase::Display_Activator || type == CSMWorld::ColumnBase::Display_Static || type == CSMWorld::ColumnBase::Display_Weapon); } -bool CSMWorld::TableMimeData::isReferencable(CSMWorld::UniversalId::Type type) const +bool CSMWorld::TableMimeData::isReferencable(CSMWorld::UniversalId::Type type) { return ( type == CSMWorld::UniversalId::Type_Activator || type == CSMWorld::UniversalId::Type_Potion @@ -222,7 +222,6 @@ namespace { CSMWorld::UniversalId::Type_Race, CSMWorld::ColumnBase::Display_Race }, { CSMWorld::UniversalId::Type_Skill, CSMWorld::ColumnBase::Display_Skill }, { CSMWorld::UniversalId::Type_Class, CSMWorld::ColumnBase::Display_Class }, - { CSMWorld::UniversalId::Type_Class, CSMWorld::ColumnBase::Display_Class }, { CSMWorld::UniversalId::Type_Faction, CSMWorld::ColumnBase::Display_Faction }, { CSMWorld::UniversalId::Type_Sound, CSMWorld::ColumnBase::Display_Sound }, { CSMWorld::UniversalId::Type_Region, CSMWorld::ColumnBase::Display_Region }, @@ -264,6 +263,7 @@ namespace { CSMWorld::UniversalId::Type_SoundRes, CSMWorld::ColumnBase::Display_SoundRes }, { CSMWorld::UniversalId::Type_Texture, CSMWorld::ColumnBase::Display_Texture }, { CSMWorld::UniversalId::Type_Video, CSMWorld::ColumnBase::Display_Video }, + { CSMWorld::UniversalId::Type_Global, CSMWorld::ColumnBase::Display_GlobalVariable }, { CSMWorld::UniversalId::Type_None, CSMWorld::ColumnBase::Display_None } // end marker }; diff --git a/apps/opencs/model/world/tablemimedata.hpp b/apps/opencs/model/world/tablemimedata.hpp index 85c243944..06d252435 100644 --- a/apps/opencs/model/world/tablemimedata.hpp +++ b/apps/opencs/model/world/tablemimedata.hpp @@ -59,10 +59,10 @@ namespace CSMWorld static CSMWorld::ColumnBase::Display convertEnums(CSMWorld::UniversalId::Type type); + static bool isReferencable(CSMWorld::UniversalId::Type type); private: - bool isReferencable(CSMWorld::UniversalId::Type type) const; bool isReferencable(CSMWorld::ColumnBase::Display type) const; }; } -#endif // TABLEMIMEDATA_H \ No newline at end of file +#endif // TABLEMIMEDATA_H diff --git a/apps/opencs/model/world/universalid.cpp b/apps/opencs/model/world/universalid.cpp index 7ee767354..50ac846db 100644 --- a/apps/opencs/model/world/universalid.cpp +++ b/apps/opencs/model/world/universalid.cpp @@ -46,10 +46,16 @@ namespace { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Filters, "Filters", 0 }, { CSMWorld::UniversalId::Class_ResourceList, CSMWorld::UniversalId::Type_Meshes, "Meshes", 0 }, { CSMWorld::UniversalId::Class_ResourceList, CSMWorld::UniversalId::Type_Icons, "Icons", 0 }, - { CSMWorld::UniversalId::Class_ResourceList, CSMWorld::UniversalId::Type_Musics, "Musics", 0 }, + { CSMWorld::UniversalId::Class_ResourceList, CSMWorld::UniversalId::Type_Musics, "Music Files", 0 }, { CSMWorld::UniversalId::Class_ResourceList, CSMWorld::UniversalId::Type_SoundsRes, "Sound Files", 0 }, { CSMWorld::UniversalId::Class_ResourceList, CSMWorld::UniversalId::Type_Textures, "Textures", 0 }, { CSMWorld::UniversalId::Class_ResourceList, CSMWorld::UniversalId::Type_Videos, "Videos", 0 }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_DebugProfiles, "Debug Profiles", 0 }, + { CSMWorld::UniversalId::Class_Transient, CSMWorld::UniversalId::Type_RunLog, "Run Log", 0 }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_SoundGens, "Sound Generators", 0 }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_MagicEffects, "Magic Effects", 0 }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Pathgrids, "Pathgrids", 0 }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_StartScripts, "Start Scripts", 0 }, { CSMWorld::UniversalId::Class_None, CSMWorld::UniversalId::Type_None, 0, 0 } // end marker }; @@ -109,6 +115,11 @@ namespace { CSMWorld::UniversalId::Class_Resource, CSMWorld::UniversalId::Type_SoundRes, "Sound File", 0 }, { CSMWorld::UniversalId::Class_Resource, CSMWorld::UniversalId::Type_Texture, "Texture", 0 }, { CSMWorld::UniversalId::Class_Resource, CSMWorld::UniversalId::Type_Video, "Video", 0 }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_DebugProfile, "Debug Profile", 0 }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_SoundGen, "Sound Generator", 0 }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_MagicEffect, "Magic Effect", 0 }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Pathgrid, "Pathgrid", 0 }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_StartScript, "Start Script", 0 }, { CSMWorld::UniversalId::Class_None, CSMWorld::UniversalId::Type_None, 0, 0 } // end marker }; @@ -122,6 +133,7 @@ namespace } CSMWorld::UniversalId::UniversalId (const std::string& universalId) +: mIndex(0) { std::string::size_type index = universalId.find (':'); @@ -319,7 +331,7 @@ std::string CSMWorld::UniversalId::getIcon() const for (int i=0; typeData[i].mName; ++i) if (typeData[i].mType==mType) - return typeData[i].mIcon ? typeData[i].mIcon : ""; + return typeData[i].mIcon ? typeData[i].mIcon : ":placeholder"; throw std::logic_error ("failed to retrieve UniversalId type icon"); } diff --git a/apps/opencs/model/world/universalid.hpp b/apps/opencs/model/world/universalid.hpp index 3d3f215d6..a716aec03 100644 --- a/apps/opencs/model/world/universalid.hpp +++ b/apps/opencs/model/world/universalid.hpp @@ -119,10 +119,21 @@ namespace CSMWorld Type_Textures, Type_Texture, Type_Videos, - Type_Video + Type_Video, + Type_DebugProfiles, + Type_DebugProfile, + Type_SoundGens, + Type_SoundGen, + Type_MagicEffects, + Type_MagicEffect, + Type_Pathgrids, + Type_Pathgrid, + Type_StartScripts, + Type_StartScript, + Type_RunLog }; - enum { NumberOfTypes = Type_BodyPart+1 }; + enum { NumberOfTypes = Type_RunLog+1 }; private: diff --git a/apps/opencs/view/doc/adjusterwidget.cpp b/apps/opencs/view/doc/adjusterwidget.cpp index 09e58690f..6571ad7c8 100644 --- a/apps/opencs/view/doc/adjusterwidget.cpp +++ b/apps/opencs/view/doc/adjusterwidget.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -72,8 +73,11 @@ void CSVDoc::AdjusterWidget::setName (const QString& name, bool addon) { boost::filesystem::path path (name.toUtf8().data()); - bool isLegacyPath = (path.extension() == ".esm" || - path.extension() == ".esp"); + std::string extension = path.extension().string(); + boost::algorithm::to_lower(extension); + + bool isLegacyPath = (extension == ".esm" || + extension == ".esp"); bool isFilePathChanged = (path.parent_path().string() != mLocalData.string()); diff --git a/apps/opencs/view/doc/filedialog.cpp b/apps/opencs/view/doc/filedialog.cpp index ab56415a1..8dd83f24a 100644 --- a/apps/opencs/view/doc/filedialog.cpp +++ b/apps/opencs/view/doc/filedialog.cpp @@ -18,7 +18,7 @@ #include "adjusterwidget.hpp" CSVDoc::FileDialog::FileDialog(QWidget *parent) : - QDialog(parent), mSelector (0), mFileWidget (0), mAdjusterWidget (0) + QDialog(parent), mSelector (0), mFileWidget (0), mAdjusterWidget (0), mDialogBuilt(false), mAction(ContentAction_Undefined) { ui.setupUi (this); resize(400, 400); @@ -70,11 +70,15 @@ void CSVDoc::FileDialog::showDialog (ContentAction action) mAdjusterWidget->setFilenameCheck (mAction == ContentAction_New); - //connections common to both dialog view flavors - connect (mSelector, SIGNAL (signalCurrentGamefileIndexChanged (int)), - this, SLOT (slotUpdateAcceptButton (int))); + if(!mDialogBuilt) + { + //connections common to both dialog view flavors + connect (mSelector, SIGNAL (signalCurrentGamefileIndexChanged (int)), + this, SLOT (slotUpdateAcceptButton (int))); - connect (ui.projectButtonBox, SIGNAL (rejected()), this, SLOT (slotRejected())); + connect (ui.projectButtonBox, SIGNAL (rejected()), this, SLOT (slotRejected())); + mDialogBuilt = true; + } show(); raise(); @@ -85,22 +89,26 @@ void CSVDoc::FileDialog::buildNewFileView() { setWindowTitle(tr("Create a new addon")); - QPushButton* createButton = ui.projectButtonBox->button (QDialogButtonBox::Ok); - createButton->setText ("Create"); - createButton->setEnabled (false); + QPushButton* createButton = ui.projectButtonBox->button (QDialogButtonBox::Ok); + createButton->setText ("Create"); + createButton->setEnabled (false); - mFileWidget = new FileWidget (this); + if(!mFileWidget) + { + mFileWidget = new FileWidget (this); - mFileWidget->setType (true); - mFileWidget->extensionLabelIsVisible(true); + mFileWidget->setType (true); + mFileWidget->extensionLabelIsVisible(true); - ui.projectGroupBoxLayout->insertWidget (0, mFileWidget); + connect (mFileWidget, SIGNAL (nameChanged (const QString&, bool)), + mAdjusterWidget, SLOT (setName (const QString&, bool))); + + connect (mFileWidget, SIGNAL (nameChanged(const QString &, bool)), + this, SLOT (slotUpdateAcceptButton(const QString &, bool))); - connect (mFileWidget, SIGNAL (nameChanged (const QString&, bool)), - mAdjusterWidget, SLOT (setName (const QString&, bool))); + } - connect (mFileWidget, SIGNAL (nameChanged(const QString &, bool)), - this, SLOT (slotUpdateAcceptButton(const QString &, bool))); + ui.projectGroupBoxLayout->insertWidget (0, mFileWidget); connect (ui.projectButtonBox, SIGNAL (accepted()), this, SLOT (slotNewFile())); } @@ -109,16 +117,25 @@ void CSVDoc::FileDialog::buildOpenFileView() { setWindowTitle(tr("Open")); ui.projectGroupBox->setTitle (QString("")); + ui.projectButtonBox->button(QDialogButtonBox::Ok)->setText ("Open"); + if(mSelector->isGamefileSelected()) + ui.projectButtonBox->button(QDialogButtonBox::Ok)->setEnabled (true); + else + ui.projectButtonBox->button(QDialogButtonBox::Ok)->setEnabled (false); - ui.projectButtonBox->button(QDialogButtonBox::Ok)->setEnabled (false); - - connect (mSelector, SIGNAL (signalAddonFileSelected (int)), this, SLOT (slotUpdateAcceptButton (int))); - connect (mSelector, SIGNAL (signalAddonFileUnselected (int)), this, SLOT (slotUpdateAcceptButton (int))); + if(!mDialogBuilt) + { + connect (mSelector, SIGNAL (signalAddonDataChanged (const QModelIndex&, const QModelIndex&)), this, SLOT (slotAddonDataChanged(const QModelIndex&, const QModelIndex&))); + } + connect (ui.projectButtonBox, SIGNAL (accepted()), this, SLOT (slotOpenFile())); +} - connect (ui.projectButtonBox, SIGNAL (accepted()), this, SLOT (slotOpenFile())); +void CSVDoc::FileDialog::slotAddonDataChanged(const QModelIndex &topleft, const QModelIndex &bottomright) +{ + slotUpdateAcceptButton(0); } -void CSVDoc::FileDialog::slotUpdateAcceptButton (int) +void CSVDoc::FileDialog::slotUpdateAcceptButton(int) { QString name = ""; @@ -130,7 +147,7 @@ void CSVDoc::FileDialog::slotUpdateAcceptButton (int) void CSVDoc::FileDialog::slotUpdateAcceptButton(const QString &name, bool) { - bool success = (mSelector->selectedFiles().size() > 0); + bool success = !mSelector->selectedFiles().empty(); bool isNew = (mAction == ContentAction_New); @@ -156,12 +173,26 @@ QString CSVDoc::FileDialog::filename() const void CSVDoc::FileDialog::slotRejected() { emit rejected(); + disconnect (ui.projectButtonBox, SIGNAL (accepted()), this, SLOT (slotNewFile())); + disconnect (ui.projectButtonBox, SIGNAL (accepted()), this, SLOT (slotOpenFile())); + if(mFileWidget) + { + delete mFileWidget; + mFileWidget = NULL; + } close(); } void CSVDoc::FileDialog::slotNewFile() { emit signalCreateNewFile (mAdjusterWidget->getPath()); + if(mFileWidget) + { + delete mFileWidget; + mFileWidget = NULL; + } + disconnect (ui.projectButtonBox, SIGNAL (accepted()), this, SLOT (slotNewFile())); + close(); } void CSVDoc::FileDialog::slotOpenFile() @@ -171,4 +202,6 @@ void CSVDoc::FileDialog::slotOpenFile() mAdjusterWidget->setName (file->filePath(), !file->isGameFile()); emit signalOpenFiles (mAdjusterWidget->getPath()); + disconnect (ui.projectButtonBox, SIGNAL (accepted()), this, SLOT (slotOpenFile())); + close(); } diff --git a/apps/opencs/view/doc/filedialog.hpp b/apps/opencs/view/doc/filedialog.hpp index d9fd56943..3c23a5cb5 100644 --- a/apps/opencs/view/doc/filedialog.hpp +++ b/apps/opencs/view/doc/filedialog.hpp @@ -37,6 +37,7 @@ namespace CSVDoc ContentAction mAction; FileWidget *mFileWidget; AdjusterWidget *mAdjusterWidget; + bool mDialogBuilt; public: @@ -69,6 +70,7 @@ namespace CSVDoc void slotUpdateAcceptButton (int); void slotUpdateAcceptButton (const QString &, bool); void slotRejected(); + void slotAddonDataChanged(const QModelIndex& topleft, const QModelIndex& bottomright); }; } #endif // FILEDIALOG_HPP diff --git a/apps/opencs/view/doc/filewidget.cpp b/apps/opencs/view/doc/filewidget.cpp index 9cd2fad42..f18fe695a 100644 --- a/apps/opencs/view/doc/filewidget.cpp +++ b/apps/opencs/view/doc/filewidget.cpp @@ -17,7 +17,7 @@ CSVDoc::FileWidget::FileWidget (QWidget *parent) : QWidget (parent), mAddon (fal QHBoxLayout *layout = new QHBoxLayout (this); mInput = new QLineEdit (this); - mInput->setValidator (new QRegExpValidator(QRegExp("^[a-zA-Z0-9\\s]*$"))); + mInput->setValidator (new QRegExpValidator(QRegExp("^[a-zA-Z0-9_-\\s]*$"))); layout->addWidget (mInput, 1); diff --git a/apps/opencs/view/doc/globaldebugprofilemenu.cpp b/apps/opencs/view/doc/globaldebugprofilemenu.cpp new file mode 100644 index 000000000..82bd96326 --- /dev/null +++ b/apps/opencs/view/doc/globaldebugprofilemenu.cpp @@ -0,0 +1,93 @@ + +#include "globaldebugprofilemenu.hpp" + +#include +#include + +#include + +#include "../../model/world/idtable.hpp" +#include "../../model/world/record.hpp" + +void CSVDoc::GlobalDebugProfileMenu::rebuild() +{ + clear(); + + delete mActions; + mActions = 0; + + int idColumn = mDebugProfiles->findColumnIndex (CSMWorld::Columns::ColumnId_Id); + int stateColumn = mDebugProfiles->findColumnIndex (CSMWorld::Columns::ColumnId_Modification); + int globalColumn = mDebugProfiles->findColumnIndex ( + CSMWorld::Columns::ColumnId_GlobalProfile); + + int size = mDebugProfiles->rowCount(); + + std::vector ids; + + for (int i=0; idata (mDebugProfiles->index (i, stateColumn)).toInt(); + + bool global = mDebugProfiles->data (mDebugProfiles->index (i, globalColumn)).toInt(); + + if (state!=CSMWorld::RecordBase::State_Deleted && global) + ids.push_back ( + mDebugProfiles->data (mDebugProfiles->index (i, idColumn)).toString()); + } + + mActions = new QActionGroup (this); + connect (mActions, SIGNAL (triggered (QAction *)), this, SLOT (actionTriggered (QAction *))); + + std::sort (ids.begin(), ids.end()); + + for (std::vector::const_iterator iter (ids.begin()); iter!=ids.end(); ++iter) + { + mActions->addAction (addAction (*iter)); + } +} + +CSVDoc::GlobalDebugProfileMenu::GlobalDebugProfileMenu (CSMWorld::IdTable *debugProfiles, + QWidget *parent) +: QMenu (parent), mDebugProfiles (debugProfiles), mActions (0) +{ + rebuild(); + + connect (mDebugProfiles, SIGNAL (rowsAboutToBeRemoved (const QModelIndex&, int, int)), + this, SLOT (profileAboutToBeRemoved (const QModelIndex&, int, int))); + + connect (mDebugProfiles, SIGNAL (rowsInserted (const QModelIndex&, int, int)), + this, SLOT (profileInserted (const QModelIndex&, int, int))); + + connect (mDebugProfiles, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), + this, SLOT (profileChanged (const QModelIndex&, const QModelIndex&))); +} + +void CSVDoc::GlobalDebugProfileMenu::updateActions (bool running) +{ + if (mActions) + mActions->setEnabled (!running); +} + +void CSVDoc::GlobalDebugProfileMenu::profileAboutToBeRemoved (const QModelIndex& parent, + int start, int end) +{ + rebuild(); +} + +void CSVDoc::GlobalDebugProfileMenu::profileInserted (const QModelIndex& parent, int start, + int end) +{ + rebuild(); +} + +void CSVDoc::GlobalDebugProfileMenu::profileChanged (const QModelIndex& topLeft, + const QModelIndex& bottomRight) +{ + rebuild(); +} + +void CSVDoc::GlobalDebugProfileMenu::actionTriggered (QAction *action) +{ + emit triggered (std::string (action->text().toUtf8().constData())); +} \ No newline at end of file diff --git a/apps/opencs/view/doc/globaldebugprofilemenu.hpp b/apps/opencs/view/doc/globaldebugprofilemenu.hpp new file mode 100644 index 000000000..0d7906cce --- /dev/null +++ b/apps/opencs/view/doc/globaldebugprofilemenu.hpp @@ -0,0 +1,49 @@ +#ifndef CSV_DOC_GLOBALDEBUGPROFILEMENU_H +#define CSV_DOC_GLOBALDEBUGPROFILEMENU_H + +#include + +class QModelIndex; +class QActionGroup; + +namespace CSMWorld +{ + class IdTable; +} + +namespace CSVDoc +{ + class GlobalDebugProfileMenu : public QMenu + { + Q_OBJECT + + CSMWorld::IdTable *mDebugProfiles; + QActionGroup *mActions; + + private: + + void rebuild(); + + public: + + GlobalDebugProfileMenu (CSMWorld::IdTable *debugProfiles, QWidget *parent = 0); + + void updateActions (bool running); + + private slots: + + void profileAboutToBeRemoved (const QModelIndex& parent, int start, int end); + + void profileInserted (const QModelIndex& parent, int start, int end); + + void profileChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); + + void actionTriggered (QAction *action); + + signals: + + void triggered (const std::string& profile); + }; +} + +#endif diff --git a/apps/opencs/view/doc/loader.cpp b/apps/opencs/view/doc/loader.cpp index ca7c93f9d..046eb5229 100644 --- a/apps/opencs/view/doc/loader.cpp +++ b/apps/opencs/view/doc/loader.cpp @@ -18,7 +18,7 @@ void CSVDoc::LoadingDocument::closeEvent (QCloseEvent *event) } CSVDoc::LoadingDocument::LoadingDocument (CSMDoc::Document *document) -: mDocument (document), mAborted (false), mMessages (0) +: mDocument (document), mAborted (false), mMessages (0), mTotalRecords (0) { setWindowTitle (("Opening " + document->getSavePath().filename().string()).c_str()); @@ -104,7 +104,7 @@ void CSVDoc::LoadingDocument::nextRecord (int records) void CSVDoc::LoadingDocument::abort (const std::string& error) { mAborted = true; - mError->setText (QString::fromUtf8 (("Loading failed: " + error).c_str())); + mError->setText (QString::fromUtf8 (("Loading failed: " + error + "").c_str())); mButtons->setStandardButtons (QDialogButtonBox::Close); } @@ -199,4 +199,4 @@ void CSVDoc::Loader::loadMessage (CSMDoc::Document *document, const std::string& if (iter!=mDocuments.end()) iter->second->addMessage (message); -} \ No newline at end of file +} diff --git a/apps/opencs/view/doc/runlogsubview.cpp b/apps/opencs/view/doc/runlogsubview.cpp new file mode 100644 index 000000000..68e888e8d --- /dev/null +++ b/apps/opencs/view/doc/runlogsubview.cpp @@ -0,0 +1,20 @@ + +#include "runlogsubview.hpp" + +#include + +CSVDoc::RunLogSubView::RunLogSubView (const CSMWorld::UniversalId& id, + CSMDoc::Document& document) +: SubView (id) +{ + QTextEdit *edit = new QTextEdit (this); + edit->setDocument (document.getRunLog()); + edit->setReadOnly (true); + + setWidget (edit); +} + +void CSVDoc::RunLogSubView::setEditLock (bool locked) +{ + // ignored since this SubView does not have editing +} \ No newline at end of file diff --git a/apps/opencs/view/doc/runlogsubview.hpp b/apps/opencs/view/doc/runlogsubview.hpp new file mode 100644 index 000000000..cfb676a37 --- /dev/null +++ b/apps/opencs/view/doc/runlogsubview.hpp @@ -0,0 +1,20 @@ +#ifndef CSV_DOC_RUNLOGSUBVIEW_H +#define CSV_DOC_RUNLOGSUBVIEW_H + +#include "subview.hpp" + +namespace CSVDoc +{ + class RunLogSubView : public SubView + { + Q_OBJECT + + public: + + RunLogSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document); + + virtual void setEditLock (bool locked); + }; +} + +#endif diff --git a/apps/opencs/view/doc/startup.cpp b/apps/opencs/view/doc/startup.cpp index 799a07e14..58a46c603 100644 --- a/apps/opencs/view/doc/startup.cpp +++ b/apps/opencs/view/doc/startup.cpp @@ -96,7 +96,7 @@ QWidget *CSVDoc::StartupDialogue::createTools() CSVDoc::StartupDialogue::StartupDialogue() : mWidth (0), mColumn (2) { - setWindowTitle ("Open CS"); + setWindowTitle ("OpenMW-CS"); QVBoxLayout *layout = new QVBoxLayout (this); diff --git a/apps/opencs/view/doc/subview.cpp b/apps/opencs/view/doc/subview.cpp index a80d21cb2..a399b5b5b 100644 --- a/apps/opencs/view/doc/subview.cpp +++ b/apps/opencs/view/doc/subview.cpp @@ -1,10 +1,14 @@ #include "subview.hpp" -CSVDoc::SubView::SubView (const CSMWorld::UniversalId& id) : mUniversalId (id) +#include "view.hpp" + +CSVDoc::SubView::SubView (const CSMWorld::UniversalId& id) + : mUniversalId (id) { /// \todo add a button to the title bar that clones this sub view setWindowTitle (QString::fromUtf8 (mUniversalId.toString().c_str())); + setAttribute(Qt::WA_DeleteOnClose); } CSMWorld::UniversalId CSVDoc::SubView::getUniversalId() const @@ -24,3 +28,18 @@ void CSVDoc::SubView::setUniversalId (const CSMWorld::UniversalId& id) mUniversalId = id; setWindowTitle (mUniversalId.toString().c_str()); } + +void CSVDoc::SubView::closeEvent (QCloseEvent *event) +{ + emit updateSubViewIndicies (this); +} + +std::string CSVDoc::SubView::getTitle() const +{ + return mUniversalId.toString(); +} + +void CSVDoc::SubView::closeRequest() +{ + emit closeRequest (this); +} diff --git a/apps/opencs/view/doc/subview.hpp b/apps/opencs/view/doc/subview.hpp index 733a75bb0..a8aa3cda1 100644 --- a/apps/opencs/view/doc/subview.hpp +++ b/apps/opencs/view/doc/subview.hpp @@ -18,6 +18,8 @@ namespace CSMWorld namespace CSVDoc { + class View; + class SubView : public QDockWidget { Q_OBJECT @@ -27,7 +29,9 @@ namespace CSVDoc // not implemented SubView (const SubView&); SubView& operator= (SubView&); + protected: + void setUniversalId(const CSMWorld::UniversalId& id); public: @@ -44,13 +48,27 @@ namespace CSVDoc virtual void useHint (const std::string& hint); ///< Default implementation: ignored + virtual std::string getTitle() const; + + virtual void updateUserSetting (const QString& name, const QStringList& value); + + private: + + void closeEvent (QCloseEvent *event); + signals: void focusId (const CSMWorld::UniversalId& universalId, const std::string& hint); - public slots: - virtual void updateUserSetting - (const QString &, const QStringList &); + void closeRequest (SubView *subView); + + void updateTitle(); + + void updateSubViewIndicies (SubView *view = 0); + + protected slots: + + void closeRequest(); }; } diff --git a/apps/opencs/view/doc/view.cpp b/apps/opencs/view/doc/view.cpp index 4868f20ff..211f74187 100644 --- a/apps/opencs/view/doc/view.cpp +++ b/apps/opencs/view/doc/view.cpp @@ -8,10 +8,13 @@ #include #include #include +#include #include "../../model/doc/document.hpp" #include "../../model/settings/usersettings.hpp" +#include "../../model/world/idtable.hpp" + #include "../world/subviews.hpp" #include "../tools/subviews.hpp" @@ -19,11 +22,19 @@ #include "viewmanager.hpp" #include "operations.hpp" #include "subview.hpp" +#include "globaldebugprofilemenu.hpp" +#include "runlogsubview.hpp" +#include "subviewfactoryimp.hpp" void CSVDoc::View::closeEvent (QCloseEvent *event) { if (!mViewManager.closeRequest (this)) event->ignore(); + else + { + // closeRequest() returns true if last document + mViewManager.removeDocAndView(mDocument); + } } void CSVDoc::View::setupFileMenu() @@ -93,6 +104,10 @@ void CSVDoc::View::setupViewMenu() mShowStatusBar = new QAction (tr ("Show Status Bar"), this); mShowStatusBar->setCheckable (true); connect (mShowStatusBar, SIGNAL (toggled (bool)), this, SLOT (toggleShowStatusBar (bool))); + std::string showStatusBar = + CSMSettings::UserSettings::instance().settingValue("window/show-statusbar").toStdString(); + if(showStatusBar == "true") + mShowStatusBar->setChecked(true); view->addAction (mShowStatusBar); QAction *filters = new QAction (tr ("Filters"), this); @@ -120,6 +135,10 @@ void CSVDoc::View::setupWorldMenu() connect (references, SIGNAL (triggered()), this, SLOT (addReferencesSubView())); world->addAction (references); + QAction *grid = new QAction (tr ("Pathgrid"), this); + connect (grid, SIGNAL (triggered()), this, SLOT (addPathgridSubView())); + world->addAction (grid); + world->addSeparator(); // items that don't represent single record lists follow here QAction *regionMap = new QAction (tr ("Region Map"), this); @@ -150,6 +169,14 @@ void CSVDoc::View::setupMechanicsMenu() QAction *enchantments = new QAction (tr ("Enchantments"), this); connect (enchantments, SIGNAL (triggered()), this, SLOT (addEnchantmentsSubView())); mechanics->addAction (enchantments); + + QAction *effects = new QAction (tr ("Magic Effects"), this); + connect (effects, SIGNAL (triggered()), this, SLOT (addMagicEffectsSubView())); + mechanics->addAction (effects); + + QAction *startScripts = new QAction (tr ("Start Scripts"), this); + connect (startScripts, SIGNAL (triggered()), this, SLOT (addStartScriptsSubView())); + mechanics->addAction (startScripts); } void CSVDoc::View::setupCharacterMenu() @@ -205,6 +232,10 @@ void CSVDoc::View::setupAssetsMenu() connect (sounds, SIGNAL (triggered()), this, SLOT (addSoundsSubView())); assets->addAction (sounds); + QAction *soundGens = new QAction (tr ("Sound Generators"), this); + connect (soundGens, SIGNAL (triggered()), this, SLOT (addSoundGensSubView())); + assets->addAction (soundGens); + assets->addSeparator(); // resources follow here QAction *meshes = new QAction (tr ("Meshes"), this); @@ -232,6 +263,35 @@ void CSVDoc::View::setupAssetsMenu() assets->addAction (videos); } +void CSVDoc::View::setupDebugMenu() +{ + QMenu *debug = menuBar()->addMenu (tr ("Debug")); + + QAction *profiles = new QAction (tr ("Debug Profiles"), this); + connect (profiles, SIGNAL (triggered()), this, SLOT (addDebugProfilesSubView())); + debug->addAction (profiles); + + debug->addSeparator(); + + mGlobalDebugProfileMenu = new GlobalDebugProfileMenu ( + &dynamic_cast (*mDocument->getData().getTableModel ( + CSMWorld::UniversalId::Type_DebugProfiles)), this); + + connect (mGlobalDebugProfileMenu, SIGNAL (triggered (const std::string&)), + this, SLOT (run (const std::string&))); + + QAction *runDebug = debug->addMenu (mGlobalDebugProfileMenu); + runDebug->setText (tr ("Run OpenMW")); + + mStopDebug = new QAction (tr ("Shutdown OpenMW"), this); + connect (mStopDebug, SIGNAL (triggered()), this, SLOT (stop())); + debug->addAction (mStopDebug); + + QAction *runLog = new QAction (tr ("Run Log"), this); + connect (runLog, SIGNAL (triggered()), this, SLOT (addRunLogSubView())); + debug->addAction (runLog); +} + void CSVDoc::View::setupUi() { setupFileMenu(); @@ -241,6 +301,7 @@ void CSVDoc::View::setupUi() setupMechanicsMenu(); setupCharacterMenu(); setupAssetsMenu(); + setupDebugMenu(); } void CSVDoc::View::updateTitle() @@ -255,12 +316,51 @@ void CSVDoc::View::updateTitle() if (mViewTotal>1) stream << " [" << (mViewIndex+1) << "/" << mViewTotal << "]"; + CSMSettings::UserSettings &userSettings = CSMSettings::UserSettings::instance(); + + bool hideTitle = userSettings.setting ("window/hide-subview", QString ("false"))=="true" && + mSubViews.size()==1 && !mSubViews.at (0)->isFloating(); + + if (hideTitle) + stream << " - " << mSubViews.at (0)->getTitle(); + setWindowTitle (stream.str().c_str()); } +void CSVDoc::View::updateSubViewIndicies(SubView *view) +{ + if(view && mSubViews.contains(view)) + mSubViews.removeOne(view); + + CSMSettings::UserSettings &userSettings = CSMSettings::UserSettings::instance(); + + bool hideTitle = userSettings.setting ("window/hide-subview", QString ("false"))=="true" && + mSubViews.size()==1 && !mSubViews.at (0)->isFloating(); + + updateTitle(); + + foreach (SubView *subView, mSubViews) + { + if (!subView->isFloating()) + { + if (hideTitle) + { + subView->setTitleBarWidget (new QWidget (this)); + subView->setWindowTitle (QString::fromUtf8 (subView->getTitle().c_str())); + } + else + { + delete subView->titleBarWidget(); + subView->setTitleBarWidget (0); + } + } + } +} + void CSVDoc::View::updateActions() { bool editing = !(mDocument->getState() & CSMDoc::State_Locked); + bool running = mDocument->getState() & CSMDoc::State_Running; for (std::vector::iterator iter (mEditingActions.begin()); iter!=mEditingActions.end(); ++iter) (*iter)->setEnabled (editing); @@ -268,21 +368,31 @@ void CSVDoc::View::updateActions() mUndo->setEnabled (editing & mDocument->getUndoStack().canUndo()); mRedo->setEnabled (editing & mDocument->getUndoStack().canRedo()); - mSave->setEnabled (!(mDocument->getState() & CSMDoc::State_Saving)); + mSave->setEnabled (!(mDocument->getState() & CSMDoc::State_Saving) && !running); mVerify->setEnabled (!(mDocument->getState() & CSMDoc::State_Verifying)); + + mGlobalDebugProfileMenu->updateActions (running); + mStopDebug->setEnabled (running); } CSVDoc::View::View (ViewManager& viewManager, CSMDoc::Document *document, int totalViews) : mViewManager (viewManager), mDocument (document), mViewIndex (totalViews-1), mViewTotal (totalViews) { - QString width = CSMSettings::UserSettings::instance().settingValue - ("Window Size/Width"); + int width = CSMSettings::UserSettings::instance().settingValue + ("window/default-width").toInt(); - QString height = CSMSettings::UserSettings::instance().settingValue - ("Window Size/Height"); + int height = CSMSettings::UserSettings::instance().settingValue + ("window/default-height").toInt(); - resize (width.toInt(), height.toInt()); + width = std::max(width, 300); + height = std::max(height, 300); + + // trick to get the window decorations and their sizes + show(); + hide(); + resize (width - (frameGeometry().width() - geometry().width()), + height - (frameGeometry().height() - geometry().height())); mSubViewWindow.setDockOptions (QMainWindow::AllowNestedDocks); @@ -295,9 +405,13 @@ CSVDoc::View::View (ViewManager& viewManager, CSMDoc::Document *document, int to setupUi(); + updateActions(); + CSVWorld::addSubViewFactories (mSubViewFactory); CSVTools::addSubViewFactories (mSubViewFactory); + mSubViewFactory.add (CSMWorld::UniversalId::Type_RunLog, new SubViewFactory); + connect (mOperations, SIGNAL (abortOperation (int)), this, SLOT (abortOperation (int))); } @@ -352,38 +466,76 @@ void CSVDoc::View::updateProgress (int current, int max, int type, int threads) void CSVDoc::View::addSubView (const CSMWorld::UniversalId& id, const std::string& hint) { - /// \todo add an user setting for limiting the number of sub views per top level view. Automatically open a new top level view if this - /// number is exceeded + CSMSettings::UserSettings &userSettings = CSMSettings::UserSettings::instance(); + + bool isReferenceable = id.getClass() == CSMWorld::UniversalId::Class_RefRecord; - /// \todo if the sub view limit setting is one, the sub view title bar should be hidden and the text in the main title bar adjusted - /// accordingly + // User setting to reuse sub views (on a per top level view basis) + bool reuse = + userSettings.setting ("window/reuse", QString("true")) == "true" ? true : false; + if(reuse) + { + foreach(SubView *sb, mSubViews) + { + bool isSubViewReferenceable = + sb->getUniversalId().getType() == CSMWorld::UniversalId::Type_Referenceable; + + if((isReferenceable && isSubViewReferenceable && id.getId() == sb->getUniversalId().getId()) + || + (!isReferenceable && id == sb->getUniversalId())) + { + sb->setFocus(); + return; + } + } + } - /// \todo add an user setting to reuse sub views (on a per document basis or on a per top level view basis) + // User setting for limiting the number of sub views per top level view. + // Automatically open a new top level view if this number is exceeded + // + // If the sub view limit setting is one, the sub view title bar is hidden and the + // text in the main title bar is adjusted accordingly + int maxSubView = userSettings.setting("window/max-subviews", QString("256")).toInt(); + if(mSubViews.size() >= maxSubView) // create a new top level view + { + mViewManager.addView(mDocument, id, hint); + + return; + } - const std::vector referenceables(CSMWorld::UniversalId::listReferenceableTypes()); SubView *view = NULL; - if(std::find(referenceables.begin(), referenceables.end(), id.getType()) != referenceables.end()) + if(isReferenceable) { view = mSubViewFactory.makeSubView (CSMWorld::UniversalId(CSMWorld::UniversalId::Type_Referenceable, id.getId()), *mDocument); - } else + } + else { view = mSubViewFactory.makeSubView (id, *mDocument); } assert(view); + view->setParent(this); + mSubViews.append(view); // only after assert if (!hint.empty()) view->useHint (hint); + int minWidth = userSettings.setting ("window/minimum-width", QString("325")).toInt(); + view->setMinimumWidth(minWidth); + view->setStatusBar (mShowStatusBar->isChecked()); mSubViewWindow.addDockWidget (Qt::TopDockWidgetArea, view); + updateSubViewIndicies(); + connect (view, SIGNAL (focusId (const CSMWorld::UniversalId&, const std::string&)), this, SLOT (addSubView (const CSMWorld::UniversalId&, const std::string&))); - connect (&CSMSettings::UserSettings::instance(), - SIGNAL (userSettingUpdated (const QString &, const QStringList &)), - view, - SLOT (updateUserSetting (const QString &, const QStringList &))); + connect (view, SIGNAL (closeRequest (SubView *)), this, SLOT (closeRequest (SubView *))); + + connect (view, SIGNAL (updateTitle()), this, SLOT (updateTitle())); + + connect (view, SIGNAL (updateSubViewIndicies (SubView *)), + this, SLOT (updateSubViewIndicies (SubView *))); view->show(); } @@ -513,6 +665,11 @@ void CSVDoc::View::addBodyPartsSubView() addSubView (CSMWorld::UniversalId::Type_BodyParts); } +void CSVDoc::View::addSoundGensSubView() +{ + addSubView (CSMWorld::UniversalId::Type_SoundGens); +} + void CSVDoc::View::addMeshesSubView() { addSubView (CSMWorld::UniversalId::Type_Meshes); @@ -533,6 +690,11 @@ void CSVDoc::View::addSoundsResSubView() addSubView (CSMWorld::UniversalId::Type_SoundsRes); } +void CSVDoc::View::addMagicEffectsSubView() +{ + addSubView (CSMWorld::UniversalId::Type_MagicEffects); +} + void CSVDoc::View::addTexturesSubView() { addSubView (CSMWorld::UniversalId::Type_Textures); @@ -543,6 +705,26 @@ void CSVDoc::View::addVideosSubView() addSubView (CSMWorld::UniversalId::Type_Videos); } +void CSVDoc::View::addDebugProfilesSubView() +{ + addSubView (CSMWorld::UniversalId::Type_DebugProfiles); +} + +void CSVDoc::View::addRunLogSubView() +{ + addSubView (CSMWorld::UniversalId::Type_RunLog); +} + +void CSVDoc::View::addPathgridSubView() +{ + addSubView (CSMWorld::UniversalId::Type_Pathgrids); +} + +void CSVDoc::View::addStartScriptsSubView() +{ + addSubView (CSMWorld::UniversalId::Type_StartScripts); +} + void CSVDoc::View::abortOperation (int type) { mDocument->abortOperation (type); @@ -571,9 +753,16 @@ void CSVDoc::View::resizeViewHeight (int height) resize (geometry().width(), height); } -void CSVDoc::View::updateUserSetting - (const QString &name, const QStringList &list) -{} +void CSVDoc::View::updateUserSetting (const QString &name, const QStringList &list) +{ + if (name=="window/hide-subview") + updateSubViewIndicies (0); + + foreach (SubView *subView, mSubViews) + { + subView->updateUserSetting (name, list); + } +} void CSVDoc::View::toggleShowStatusBar (bool show) { @@ -584,7 +773,33 @@ void CSVDoc::View::toggleShowStatusBar (bool show) } } +void CSVDoc::View::toggleStatusBar(bool checked) +{ + mShowStatusBar->setChecked(checked); +} + void CSVDoc::View::loadErrorLog() { addSubView (CSMWorld::UniversalId (CSMWorld::UniversalId::Type_LoadErrorLog, 0)); } + +void CSVDoc::View::run (const std::string& profile, const std::string& startupInstruction) +{ + mDocument->startRunning (profile, startupInstruction); +} + +void CSVDoc::View::stop() +{ + mDocument->stopRunning(); +} + +void CSVDoc::View::closeRequest (SubView *subView) +{ + CSMSettings::UserSettings &userSettings = CSMSettings::UserSettings::instance(); + + if (mSubViews.size()>1 || mViewTotal<=1 || + userSettings.setting ("window/hide-subview", QString ("false"))!="true") + subView->deleteLater(); + else if (mViewManager.closeRequest (this)) + mViewManager.removeDocAndView (mDocument); +} diff --git a/apps/opencs/view/doc/view.hpp b/apps/opencs/view/doc/view.hpp index 19171ff42..baadca85c 100644 --- a/apps/opencs/view/doc/view.hpp +++ b/apps/opencs/view/doc/view.hpp @@ -25,6 +25,7 @@ namespace CSVDoc { class ViewManager; class Operations; + class GlobalDebugProfileMenu; class View : public QMainWindow { @@ -34,15 +35,18 @@ namespace CSVDoc CSMDoc::Document *mDocument; int mViewIndex; int mViewTotal; + QList mSubViews; QAction *mUndo; QAction *mRedo; QAction *mSave; QAction *mVerify; QAction *mShowStatusBar; + QAction *mStopDebug; std::vector mEditingActions; Operations *mOperations; SubViewFactoryManager mSubViewFactory; QMainWindow mSubViewWindow; + GlobalDebugProfileMenu *mGlobalDebugProfileMenu; // not implemented @@ -67,9 +71,9 @@ namespace CSVDoc void setupAssetsMenu(); - void setupUi(); + void setupDebugMenu(); - void updateTitle(); + void setupUi(); void updateActions(); @@ -101,6 +105,8 @@ namespace CSVDoc void updateProgress (int current, int max, int type, int threads); + void toggleStatusBar(bool checked); + Operations *getOperations() const; /// Function called by view manager when user preferences are updated @@ -128,6 +134,11 @@ namespace CSVDoc void updateUserSetting (const QString &, const QStringList &); + void updateTitle(); + + // called when subviews are added or removed + void updateSubViewIndicies (SubView *view = 0); + private slots: void newView(); @@ -182,6 +193,10 @@ namespace CSVDoc void addBodyPartsSubView(); + void addSoundGensSubView(); + + void addMagicEffectsSubView(); + void addMeshesSubView(); void addIconsSubView(); @@ -194,9 +209,23 @@ namespace CSVDoc void addVideosSubView(); + void addDebugProfilesSubView(); + + void addRunLogSubView(); + + void addPathgridSubView(); + + void addStartScriptsSubView(); + void toggleShowStatusBar (bool show); void loadErrorLog(); + + void run (const std::string& profile, const std::string& startupInstruction = ""); + + void stop(); + + void closeRequest (SubView *subView); }; } diff --git a/apps/opencs/view/doc/viewmanager.cpp b/apps/opencs/view/doc/viewmanager.cpp index 6f4217aa8..5f6b6b46a 100644 --- a/apps/opencs/view/doc/viewmanager.cpp +++ b/apps/opencs/view/doc/viewmanager.cpp @@ -9,6 +9,7 @@ #include "../../model/doc/documentmanager.hpp" #include "../../model/doc/document.hpp" #include "../../model/world/columns.hpp" +#include "../../model/world/universalid.hpp" #include "../world/util.hpp" #include "../world/enumdelegate.hpp" @@ -81,7 +82,9 @@ CSVDoc::ViewManager::ViewManager (CSMDoc::DocumentManager& documentManager) { CSMWorld::ColumnBase::Display_EnchantmentType, CSMWorld::Columns::ColumnId_EnchantmentType, false }, { CSMWorld::ColumnBase::Display_BodyPartType, CSMWorld::Columns::ColumnId_BodyPartType, false }, { CSMWorld::ColumnBase::Display_MeshType, CSMWorld::Columns::ColumnId_MeshType, false }, - { CSMWorld::ColumnBase::Display_Gender, CSMWorld::Columns::ColumnId_Gender, true } + { CSMWorld::ColumnBase::Display_Gender, CSMWorld::Columns::ColumnId_Gender, true }, + { CSMWorld::ColumnBase::Display_SoundGeneratorType, CSMWorld::Columns::ColumnId_SoundGeneratorType, false }, + { CSMWorld::ColumnBase::Display_School, CSMWorld::Columns::ColumnId_School, true } }; for (std::size_t i=0; itoggleStatusBar (showStatusBar == "true"); view->show(); connect (view, SIGNAL (newGameRequest ()), this, SIGNAL (newGameRequest())); @@ -157,6 +164,14 @@ CSVDoc::View *CSVDoc::ViewManager::addView (CSMDoc::Document *document) return view; } +CSVDoc::View *CSVDoc::ViewManager::addView (CSMDoc::Document *document, const CSMWorld::UniversalId& id, const std::string& hint) +{ + View* view = addView(document); + view->addSubView(id, hint); + + return view; +} + int CSVDoc::ViewManager::countViews (const CSMDoc::Document *document) const { int count = 0; @@ -172,7 +187,7 @@ bool CSVDoc::ViewManager::closeRequest (View *view) { std::vector::iterator iter = std::find (mViews.begin(), mViews.end(), view); - bool continueWithClose = true; + bool continueWithClose = false; if (iter!=mViews.end()) { @@ -192,6 +207,24 @@ bool CSVDoc::ViewManager::closeRequest (View *view) return continueWithClose; } +// NOTE: This method assumes that it is called only if the last document +void CSVDoc::ViewManager::removeDocAndView (CSMDoc::Document *document) +{ + for (std::vector::iterator iter (mViews.begin()); iter!=mViews.end(); ++iter) + { + // the first match should also be the only match + if((*iter)->getDocument() == document) + { + mDocumentManager.removeDocument(document); + (*iter)->deleteLater(); + mViews.erase (iter); + + updateIndices(); + return; + } + } +} + bool CSVDoc::ViewManager::notifySaveOnClose (CSVDoc::View *view) { bool result = true; @@ -210,13 +243,19 @@ bool CSVDoc::ViewManager::notifySaveOnClose (CSVDoc::View *view) bool CSVDoc::ViewManager::showModifiedDocumentMessageBox (CSVDoc::View *view) { - QMessageBox messageBox; + emit closeMessageBox(); + + QMessageBox messageBox(view); CSMDoc::Document *document = view->getDocument(); + messageBox.setWindowTitle (document->getSavePath().filename().string().c_str()); messageBox.setText ("The document has been modified."); messageBox.setInformativeText ("Do you want to save your changes?"); messageBox.setStandardButtons (QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel); messageBox.setDefaultButton (QMessageBox::Save); + messageBox.setWindowModality (Qt::NonModal); + messageBox.hide(); + messageBox.show(); bool retVal = true; @@ -341,8 +380,40 @@ void CSVDoc::ViewManager::onExitWarningHandler (int state, CSMDoc::Document *doc } } +bool CSVDoc::ViewManager::removeDocument (CSVDoc::View *view) +{ + if(!notifySaveOnClose(view)) + return false; + else + { + // don't bother closing views or updating indicies, but remove from mViews + CSMDoc::Document * document = view->getDocument(); + std::vector remainingViews; + std::vector::const_iterator iter = mViews.begin(); + for (; iter!=mViews.end(); ++iter) + { + if(document == (*iter)->getDocument()) + (*iter)->setVisible(false); + else + remainingViews.push_back(*iter); + } + mDocumentManager.removeDocument(document); + mViews = remainingViews; + } + return true; +} + void CSVDoc::ViewManager::exitApplication (CSVDoc::View *view) { - if (notifySaveOnClose (view)) - QApplication::instance()->exit(); + if(!removeDocument(view)) // close the current document first + return; + + while(!mViews.empty()) // attempt to close all other documents + { + mViews.back()->activateWindow(); + mViews.back()->raise(); // raise the window to alert the user + if(!removeDocument(mViews.back())) + return; + } + // Editor exits (via a signal) when the last document is deleted } diff --git a/apps/opencs/view/doc/viewmanager.hpp b/apps/opencs/view/doc/viewmanager.hpp index 8cc92774b..cdd5ac768 100644 --- a/apps/opencs/view/doc/viewmanager.hpp +++ b/apps/opencs/view/doc/viewmanager.hpp @@ -18,6 +18,11 @@ namespace CSVWorld class CommandDelegateFactoryCollection; } +namespace CSMWorld +{ + class UniversalId; +} + namespace CSVDoc { class View; @@ -41,6 +46,7 @@ namespace CSVDoc bool notifySaveOnClose (View *view = 0); bool showModifiedDocumentMessageBox (View *view); bool showSaveInProgressMessageBox (View *view); + bool removeDocument(View *view); public: @@ -51,10 +57,13 @@ namespace CSVDoc View *addView (CSMDoc::Document *document); ///< The ownership of the returned view is not transferred. + View *addView (CSMDoc::Document *document, const CSMWorld::UniversalId& id, const std::string& hint); + int countViews (const CSMDoc::Document *document) const; ///< Return number of views for \a document. bool closeRequest (View *view); + void removeDocAndView (CSMDoc::Document *document); signals: diff --git a/apps/opencs/view/filter/filtercreator.cpp b/apps/opencs/view/filter/filtercreator.cpp deleted file mode 100644 index 640c9fe78..000000000 --- a/apps/opencs/view/filter/filtercreator.cpp +++ /dev/null @@ -1,77 +0,0 @@ - -#include "filtercreator.hpp" - -#include -#include - -#include "../../model/filter/filter.hpp" - -#include "../../model/world/data.hpp" -#include "../../model/world/commands.hpp" -#include "../../model/world/columns.hpp" -#include "../../model/world/idtable.hpp" - -std::string CSVFilter::FilterCreator::getNamespace() const -{ - switch (mScope->currentIndex()) - { - case CSMFilter::Filter::Scope_Project: return "project::"; - case CSMFilter::Filter::Scope_Session: return "session::"; - } - - return ""; -} - -void CSVFilter::FilterCreator::update() -{ - mNamespace->setText (QString::fromUtf8 (getNamespace().c_str())); - GenericCreator::update(); -} - -std::string CSVFilter::FilterCreator::getId() const -{ - return getNamespace() + GenericCreator::getId(); -} - -void CSVFilter::FilterCreator::configureCreateCommand (CSMWorld::CreateCommand& command) const -{ - int index = - dynamic_cast (*getData().getTableModel (getCollectionId())). - findColumnIndex (CSMWorld::Columns::ColumnId_Scope); - - command.addValue (index, mScope->currentIndex()); -} - -CSVFilter::FilterCreator::FilterCreator (CSMWorld::Data& data, QUndoStack& undoStack, - const CSMWorld::UniversalId& id) -: GenericCreator (data, undoStack, id) -{ - mNamespace = new QLabel ("::", this); - insertAtBeginning (mNamespace, false); - - mScope = new QComboBox (this); - - mScope->addItem ("Project"); - mScope->addItem ("Session"); - /// \todo re-enable for OpenMW 1.1 - // mScope->addItem ("Content"); - - connect (mScope, SIGNAL (currentIndexChanged (int)), this, SLOT (setScope (int))); - - insertAtBeginning (mScope, false); - - QLabel *label = new QLabel ("Scope", this); - insertAtBeginning (label, false); - - mScope->setCurrentIndex (1); -} - -void CSVFilter::FilterCreator::reset() -{ - GenericCreator::reset(); -} - -void CSVFilter::FilterCreator::setScope (int index) -{ - update(); -} diff --git a/apps/opencs/view/filter/filtercreator.hpp b/apps/opencs/view/filter/filtercreator.hpp deleted file mode 100644 index 437d01c8d..000000000 --- a/apps/opencs/view/filter/filtercreator.hpp +++ /dev/null @@ -1,43 +0,0 @@ -#ifndef CSV_FILTER_FILTERCREATOR_H -#define CSV_FILTER_FILTERCREATOR_H - -class QComboBox; -class QLabel; - -#include "../world/genericcreator.hpp" - -namespace CSVFilter -{ - class FilterCreator : public CSVWorld::GenericCreator - { - Q_OBJECT - - QComboBox *mScope; - QLabel *mNamespace; - - private: - - std::string getNamespace() const; - - protected: - - void update(); - - virtual std::string getId() const; - - virtual void configureCreateCommand (CSMWorld::CreateCommand& command) const; - - public: - - FilterCreator (CSMWorld::Data& data, QUndoStack& undoStack, - const CSMWorld::UniversalId& id); - - virtual void reset(); - - private slots: - - void setScope (int index); - }; -} - -#endif diff --git a/apps/opencs/view/filter/recordfilterbox.cpp b/apps/opencs/view/filter/recordfilterbox.cpp index ec5647618..97490d508 100644 --- a/apps/opencs/view/filter/recordfilterbox.cpp +++ b/apps/opencs/view/filter/recordfilterbox.cpp @@ -11,9 +11,11 @@ CSVFilter::RecordFilterBox::RecordFilterBox (CSMWorld::Data& data, QWidget *pare { QHBoxLayout *layout = new QHBoxLayout (this); - layout->setContentsMargins (0, 0, 0, 0); + layout->setContentsMargins (0, 6, 5, 0); - layout->addWidget (new QLabel ("Record Filter", this)); + QLabel *label = new QLabel("Record Filter", this); + label->setIndent(2); + layout->addWidget (label); mEdit = new EditWidget (data, this); diff --git a/apps/opencs/view/render/cell.cpp b/apps/opencs/view/render/cell.cpp index 1e25f42e2..ae2fad95a 100644 --- a/apps/opencs/view/render/cell.cpp +++ b/apps/opencs/view/render/cell.cpp @@ -5,10 +5,15 @@ #include #include +#include #include "../../model/world/idtable.hpp" #include "../../model/world/columns.hpp" #include "../../model/world/data.hpp" +#include "../world/physicssystem.hpp" + +#include "elements.hpp" +#include "terrainstorage.hpp" bool CSVRender::Cell::removeObject (const std::string& id) { @@ -46,7 +51,7 @@ bool CSVRender::Cell::addObjects (int start, int end) std::string id = Misc::StringUtils::lowerCase (references.data ( references.index (i, idColumn)).toString().toUtf8().constData()); - mObjects.insert (std::make_pair (id, new Object (mData, mCellNode, id, false))); + mObjects.insert (std::make_pair (id, new Object (mData, mCellNode, id, false, mPhysics))); modified = true; } } @@ -55,8 +60,8 @@ bool CSVRender::Cell::addObjects (int start, int end) } CSVRender::Cell::Cell (CSMWorld::Data& data, Ogre::SceneManager *sceneManager, - const std::string& id, const Ogre::Vector3& origin) -: mData (data), mId (Misc::StringUtils::lowerCase (id)) + const std::string& id, boost::shared_ptr physics, const Ogre::Vector3& origin) +: mData (data), mId (Misc::StringUtils::lowerCase (id)), mSceneMgr(sceneManager), mPhysics(physics), mX(0), mY(0) { mCellNode = sceneManager->getRootSceneNode()->createChildSceneNode(); mCellNode->setPosition (origin); @@ -67,10 +72,34 @@ CSVRender::Cell::Cell (CSMWorld::Data& data, Ogre::SceneManager *sceneManager, int rows = references.rowCount(); addObjects (0, rows-1); + + const CSMWorld::IdCollection& land = mData.getLand(); + int landIndex = land.searchId(mId); + if (landIndex != -1) + { + const ESM::Land* esmLand = land.getRecord(mId).get().mLand.get(); + if(esmLand && esmLand->mDataTypes&ESM::Land::DATA_VHGT) + { + mTerrain.reset(new Terrain::TerrainGrid(sceneManager, new TerrainStorage(mData), Element_Terrain, true, + Terrain::Align_XY)); + mTerrain->loadCell(esmLand->mX, + esmLand->mY); + + float verts = ESM::Land::LAND_SIZE; + float worldsize = ESM::Land::REAL_SIZE; + mX = esmLand->mX; + mY = esmLand->mY; + mPhysics->addHeightField(sceneManager, + esmLand->mLandData->mHeights, mX, mY, 0, worldsize / (verts-1), verts); + } + } } CSVRender::Cell::~Cell() { + if (mTerrain.get()) + mPhysics->removeHeightField(mSceneMgr, mX, mY); + for (std::map::iterator iter (mObjects.begin()); iter!=mObjects.end(); ++iter) delete iter->second; @@ -163,7 +192,7 @@ bool CSVRender::Cell::referenceDataChanged (const QModelIndex& topLeft, for (std::map::iterator iter (ids.begin()); iter!=ids.end(); ++iter) { mObjects.insert (std::make_pair ( - iter->first, new Object (mData, mCellNode, iter->first, false))); + iter->first, new Object (mData, mCellNode, iter->first, false, mPhysics))); modified = true; } @@ -198,4 +227,12 @@ bool CSVRender::Cell::referenceAdded (const QModelIndex& parent, int start, int return false; return addObjects (start, end); -} \ No newline at end of file +} + +float CSVRender::Cell::getTerrainHeightAt(const Ogre::Vector3 &pos) const +{ + if(mTerrain.get() != NULL) + return mTerrain->getHeightAt(pos); + else + return -std::numeric_limits::max(); +} diff --git a/apps/opencs/view/render/cell.hpp b/apps/opencs/view/render/cell.hpp index 70adebe45..73d794948 100644 --- a/apps/opencs/view/render/cell.hpp +++ b/apps/opencs/view/render/cell.hpp @@ -3,9 +3,16 @@ #include #include +#include + +#include #include +#ifndef Q_MOC_RUN +#include +#endif + #include "object.hpp" class QModelIndex; @@ -21,6 +28,11 @@ namespace CSMWorld class Data; } +namespace CSVWorld +{ + class PhysicsSystem; +} + namespace CSVRender { class Cell @@ -29,6 +41,11 @@ namespace CSVRender std::string mId; Ogre::SceneNode *mCellNode; std::map mObjects; + std::auto_ptr mTerrain; + boost::shared_ptr mPhysics; + Ogre::SceneManager *mSceneMgr; + int mX; + int mY; /// Ignored if cell does not have an object with the given ID. /// @@ -42,8 +59,8 @@ namespace CSVRender public: - Cell (CSMWorld::Data& data, Ogre::SceneManager *sceneManager, - const std::string& id, const Ogre::Vector3& origin = Ogre::Vector3 (0, 0, 0)); + Cell (CSMWorld::Data& data, Ogre::SceneManager *sceneManager, const std::string& id, + boost::shared_ptr physics, const Ogre::Vector3& origin = Ogre::Vector3 (0, 0, 0)); ~Cell(); @@ -67,6 +84,8 @@ namespace CSVRender /// \return Did this call result in a modification of the visual representation of /// this cell? bool referenceAdded (const QModelIndex& parent, int start, int end); + + float getTerrainHeightAt(const Ogre::Vector3 &pos) const; }; } diff --git a/apps/opencs/view/render/editmode.cpp b/apps/opencs/view/render/editmode.cpp new file mode 100644 index 000000000..51a137d3b --- /dev/null +++ b/apps/opencs/view/render/editmode.cpp @@ -0,0 +1,19 @@ + +#include "editmode.hpp" + +#include "worldspacewidget.hpp" + +CSVRender::EditMode::EditMode (WorldspaceWidget *worldspaceWidget, const QIcon& icon, + unsigned int mask, const QString& tooltip, QWidget *parent) +: ModeButton (icon, tooltip, parent), mWorldspaceWidget (worldspaceWidget), mMask (mask) +{} + +unsigned int CSVRender::EditMode::getInteractionMask() const +{ + return mMask; +} + +void CSVRender::EditMode::activate (CSVWidget::SceneToolbar *toolbar) +{ + mWorldspaceWidget->setInteractionMask (mMask); +} \ No newline at end of file diff --git a/apps/opencs/view/render/editmode.hpp b/apps/opencs/view/render/editmode.hpp new file mode 100644 index 000000000..c3192f8ea --- /dev/null +++ b/apps/opencs/view/render/editmode.hpp @@ -0,0 +1,28 @@ +#ifndef CSV_RENDER_EDITMODE_H +#define CSV_RENDER_EDITMODE_H + +#include "../widget/modebutton.hpp" + +namespace CSVRender +{ + class WorldspaceWidget; + + class EditMode : public CSVWidget::ModeButton + { + Q_OBJECT + + WorldspaceWidget *mWorldspaceWidget; + unsigned int mMask; + + public: + + EditMode (WorldspaceWidget *worldspaceWidget, const QIcon& icon, unsigned int mask, + const QString& tooltip = "", QWidget *parent = 0); + + unsigned int getInteractionMask() const; + + virtual void activate (CSVWidget::SceneToolbar *toolbar); + }; +} + +#endif diff --git a/apps/opencs/view/render/elements.hpp b/apps/opencs/view/render/elements.hpp new file mode 100644 index 000000000..5a37956e9 --- /dev/null +++ b/apps/opencs/view/render/elements.hpp @@ -0,0 +1,23 @@ +#ifndef CSV_RENDER_ELEMENTS_H +#define CSV_RENDER_ELEMENTS_H + +namespace CSVRender +{ + /// Visual elements in a scene + enum Elements + { + // elements that are part of the actual scene + Element_Reference = 0x1, + Element_Pathgrid = 0x2, + Element_Water = 0x4, + Element_Fog = 0x8, + Element_Terrain = 0x10, + + // control elements + Element_CellMarker = 0x10000, + Element_CellArrow = 0x20000, + Element_CellBorder = 0x40000 + }; +} + +#endif diff --git a/apps/opencs/view/render/mousestate.cpp b/apps/opencs/view/render/mousestate.cpp new file mode 100644 index 000000000..a94f4f8ab --- /dev/null +++ b/apps/opencs/view/render/mousestate.cpp @@ -0,0 +1,463 @@ +#include "mousestate.hpp" + +#include +#include +#include +#include + +#include +#include +#include + +#include "../../model/settings/usersettings.hpp" +#include "../../model/world/commands.hpp" +#include "../../model/world/idtable.hpp" +#include "../../model/world/universalid.hpp" +#include "../world/physicssystem.hpp" + +#include "elements.hpp" +#include "worldspacewidget.hpp" + +namespace CSVRender +{ + // mouse picking + // FIXME: need to virtualise mouse buttons + // + // State machine: + // + // [default] mousePressEvent->check if the mouse is pointing at an object + // if yes, create collision planes then go to [grab] + // else check for terrain + // + // [grab] mouseReleaseEvent->if same button and new obj, go to [edit] + // mouseMoveEvent->if same button, go to [drag] + // other mouse events or buttons, go back to [default] (i.e. like 'cancel') + // + // [drag] mouseReleaseEvent->if same button, place the object at the new + // location, update the document then go to [edit] + // mouseMoveEvent->update position to the user based on ray to the collision + // planes and render the object at the new location, but do not update + // the document yet + // + // [edit] TODO, probably fine positional adjustments or rotations; clone/delete? + // + // + // press press (obj) + // [default] --------> [grab] <-------------------- [edit] + // ^ (obj) | | ------> [drag] -----> ^ + // | | | move ^ | release | + // | | | | | | + // | | | +-+ | + // | | | move | + // +----------------+ +--------------------------+ + // release release + // (same obj) (new obj) + // + // + + MouseState::MouseState(WorldspaceWidget *parent) + : mParent(parent), mPhysics(parent->mDocument.getPhysics()), mSceneManager(parent->getSceneManager()) + , mCurrentObj(""), mMouseState(Mouse_Default), mOldPos(0,0), mMouseEventTimer(0), mPlane(0) + , mGrabbedSceneNode(""), mOrigObjPos(Ogre::Vector3()), mOrigMousePos(Ogre::Vector3()) + , mCurrentMousePos(Ogre::Vector3()), mOffset(0.0f) + , mColIndexPosX(0), mColIndexPosY(0), mColIndexPosZ(0), mIdTableModel(0) + { + const CSMWorld::RefCollection& references = mParent->mDocument.getData().getReferences(); + + mColIndexPosX = references.findColumnIndex(CSMWorld::Columns::ColumnId_PositionXPos); + mColIndexPosY = references.findColumnIndex(CSMWorld::Columns::ColumnId_PositionYPos); + mColIndexPosZ = references.findColumnIndex(CSMWorld::Columns::ColumnId_PositionZPos); + + mIdTableModel = static_cast( + mParent->mDocument.getData().getTableModel(CSMWorld::UniversalId::Type_Reference)); + + mMouseEventTimer = new QElapsedTimer(); + mMouseEventTimer->invalidate(); + + std::pair planeRes = planeAxis(); + mPlane = new Ogre::Plane(planeRes.first, 0); + Ogre::MeshPtr mesh = Ogre::MeshManager::getSingleton().createPlane("mouse", + Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, + *mPlane, + 300000,300000, // FIXME: use far clip dist? + 1,1, // segments + true, // normals + 1, // numTexCoordSets + 1,1, // uTile, vTile + planeRes.second // upVector + ); + } + + MouseState::~MouseState () + { + delete mMouseEventTimer; + delete mPlane; + } + + void MouseState::mouseMoveEvent (QMouseEvent *event) + { + switch(mMouseState) + { + case Mouse_Grab: + { + // check if min elapsed time to stop false detection of drag + if(!mMouseEventTimer->isValid() || !mMouseEventTimer->hasExpired(100)) // ms + break; + + mMouseEventTimer->invalidate(); + mMouseState = Mouse_Drag; + + /* FALL_THROUGH */ + } + case Mouse_Drag: + { + if(event->pos() != mOldPos) // TODO: maybe don't update less than a quantum? + { + mOldPos = event->pos(); + + // ray test against the plane to provide feedback to the user the + // relative movement of the object on the x-y plane + std::pair planeResult = mousePositionOnPlane(event->pos(), *mPlane); + if(planeResult.first) + { + if(mGrabbedSceneNode != "") + { + std::pair planeRes = planeAxis(); + Ogre::Vector3 pos = mOrigObjPos + planeRes.first*mOffset; + mSceneManager->getSceneNode(mGrabbedSceneNode)->setPosition(pos+planeResult.second-mOrigMousePos); + mCurrentMousePos = planeResult.second; + mPhysics->moveSceneNodes(mGrabbedSceneNode, pos+planeResult.second-mOrigMousePos); + updateSceneWidgets(); + } + } + } + break; + } + case Mouse_Edit: + case Mouse_Default: + { + break; // error event, ignore + } + /* NO_DEFAULT_CASE */ + } + } + + void MouseState::mousePressEvent (QMouseEvent *event) + { + switch(mMouseState) + { + case Mouse_Grab: + case Mouse_Drag: + { + break; + } + case Mouse_Edit: + case Mouse_Default: + { + if(event->buttons() & Qt::RightButton) + { + std::pair result = objectUnderCursor(event->x(), event->y()); + if(result.first == "") + break; + + mGrabbedSceneNode = result.first; + // ray test agaist the plane to get a starting position of the + // mouse in relation to the object position + std::pair planeRes = planeAxis(); + mPlane->redefine(planeRes.first, result.second); + std::pair planeResult = mousePositionOnPlane(event->pos(), *mPlane); + if(planeResult.first) + { + mOrigMousePos = planeResult.second; + mCurrentMousePos = planeResult.second; + mOffset = 0.0f; + } + + mOrigObjPos = mSceneManager->getSceneNode(mGrabbedSceneNode)->getPosition(); + mMouseEventTimer->start(); + + mMouseState = Mouse_Grab; + } + break; + } + /* NO_DEFAULT_CASE */ + } + } + + void MouseState::mouseReleaseEvent (QMouseEvent *event) + { + switch(mMouseState) + { + case Mouse_Grab: + { + std::pair result = objectUnderCursor(event->x(), event->y()); + if(result.first != "") + { + if(result.first == mCurrentObj) + { + // unselect object + mMouseState = Mouse_Default; + mCurrentObj = ""; + } + else + { + // select object + mMouseState = Mouse_Edit; + mCurrentObj = result.first; + + } + } + break; + } + case Mouse_Drag: + { + // final placement + std::pair planeResult = mousePositionOnPlane(event->pos(), *mPlane); + if(planeResult.first) + { + if(mGrabbedSceneNode != "") + { + std::pair planeRes = planeAxis(); + Ogre::Vector3 pos = mOrigObjPos+planeRes.first*mOffset+planeResult.second-mOrigMousePos; + // use the saved scene node name since the physics model has not moved yet + std::string referenceId = mPhysics->sceneNodeToRefId(mGrabbedSceneNode); + + mParent->mDocument.getUndoStack().beginMacro (QObject::tr("Move Object")); + mParent->mDocument.getUndoStack().push(new CSMWorld::ModifyCommand(*mIdTableModel, + mIdTableModel->getModelIndex(referenceId, mColIndexPosX), pos.x)); + mParent->mDocument.getUndoStack().push(new CSMWorld::ModifyCommand(*mIdTableModel, + mIdTableModel->getModelIndex(referenceId, mColIndexPosY), pos.y)); + mParent->mDocument.getUndoStack().push(new CSMWorld::ModifyCommand(*mIdTableModel, + mIdTableModel->getModelIndex(referenceId, mColIndexPosZ), pos.z)); + mParent->mDocument.getUndoStack().endMacro(); + + // FIXME: highlight current object? + //mCurrentObj = mGrabbedSceneNode; // FIXME: doesn't work? + mCurrentObj = ""; // whether the object is selected + + mMouseState = Mouse_Edit; + + // reset states + mCurrentMousePos = Ogre::Vector3(); // mouse pos to use in wheel event + mOrigMousePos = Ogre::Vector3(); // starting pos of mouse in world space + mOrigObjPos = Ogre::Vector3(); // starting pos of object in world space + mGrabbedSceneNode = ""; // id of the object + mOffset = 0.0f; // used for z-axis movement + mOldPos = QPoint(0, 0); // to calculate relative movement of mouse + } + } + break; + } + case Mouse_Edit: + case Mouse_Default: + { + // probably terrain, check + std::pair result = terrainUnderCursor(event->x(), event->y()); + if(result.first != "") + { + // FIXME: terrain editing goes here + } + break; + } + /* NO_DEFAULT_CASE */ + } + mMouseEventTimer->invalidate(); + } + + void MouseState::mouseDoubleClickEvent (QMouseEvent *event) + { + event->ignore(); + //mPhysics->toggleDebugRendering(mSceneManager); + //mParent->flagAsModified(); + } + + bool MouseState::wheelEvent (QWheelEvent *event) + { + switch(mMouseState) + { + case Mouse_Grab: + mMouseState = Mouse_Drag; + + /* FALL_THROUGH */ + case Mouse_Drag: + { + // move the object along the z axis during Mouse_Drag or Mouse_Grab + if (event->delta()) + { + // seems positive is up and negative is down + mOffset += (event->delta()/1); // FIXME: arbitrary number, make config option? + + std::pair planeRes = planeAxis(); + Ogre::Vector3 pos = mOrigObjPos + planeRes.first*mOffset; + mSceneManager->getSceneNode(mGrabbedSceneNode)->setPosition(pos+mCurrentMousePos-mOrigMousePos); + mPhysics->moveSceneNodes(mGrabbedSceneNode, pos+mCurrentMousePos-mOrigMousePos); + updateSceneWidgets(); + } + break; + } + case Mouse_Edit: + case Mouse_Default: + { + return false; + } + /* NO_DEFAULT_CASE */ + } + + return true; + } + + void MouseState::cancelDrag() + { + switch(mMouseState) + { + case Mouse_Grab: + case Mouse_Drag: + { + // cancel operation & return the object to the original position + mSceneManager->getSceneNode(mGrabbedSceneNode)->setPosition(mOrigObjPos); + // update all SceneWidgets and their SceneManagers + mPhysics->moveSceneNodes(mGrabbedSceneNode, mOrigObjPos); + updateSceneWidgets(); + + // reset states + mMouseState = Mouse_Default; + mCurrentMousePos = Ogre::Vector3(); + mOrigMousePos = Ogre::Vector3(); + mOrigObjPos = Ogre::Vector3(); + mGrabbedSceneNode = ""; + mCurrentObj = ""; + mOldPos = QPoint(0, 0); + mMouseEventTimer->invalidate(); + mOffset = 0.0f; + + break; + } + case Mouse_Edit: + case Mouse_Default: + { + break; + } + /* NO_DEFAULT_CASE */ + } + } + + //plane Z, upvector Y, mOffset z : x-y plane, wheel up/down + //plane Y, upvector X, mOffset y : y-z plane, wheel left/right + //plane X, upvector Y, mOffset x : x-z plane, wheel closer/further + std::pair MouseState::planeAxis() + { + const bool screenCoord = true; + Ogre::Vector3 dir = getCamera()->getDerivedDirection(); + + QString wheelDir = "Closer/Further"; + if(wheelDir == "Left/Right") + { + if(screenCoord) + return std::make_pair(getCamera()->getDerivedRight(), getCamera()->getDerivedUp()); + else + return std::make_pair(Ogre::Vector3::UNIT_Y, Ogre::Vector3::UNIT_Z); + } + else if(wheelDir == "Up/Down") + { + if(screenCoord) + return std::make_pair(getCamera()->getDerivedUp(), Ogre::Vector3(-dir.x, -dir.y, -dir.z)); + else + return std::make_pair(Ogre::Vector3::UNIT_Z, Ogre::Vector3::UNIT_X); + } + else + { + if(screenCoord) + return std::make_pair(Ogre::Vector3(-dir.x, -dir.y, -dir.z), getCamera()->getDerivedRight()); + else + return std::make_pair(Ogre::Vector3::UNIT_X, Ogre::Vector3::UNIT_Y); + } + } + + std::pair MouseState::mousePositionOnPlane(const QPoint &pos, const Ogre::Plane &plane) + { + // using a really small value seems to mess up with the projections + float nearClipDistance = getCamera()->getNearClipDistance(); // save existing + getCamera()->setNearClipDistance(10.0f); // arbitrary number + Ogre::Ray mouseRay = getCamera()->getCameraToViewportRay( + (float) pos.x() / getViewport()->getActualWidth(), + (float) pos.y() / getViewport()->getActualHeight()); + getCamera()->setNearClipDistance(nearClipDistance); // restore + std::pair planeResult = mouseRay.intersects(plane); + + if(planeResult.first) + return std::make_pair(true, mouseRay.getPoint(planeResult.second)); + else + return std::make_pair(false, Ogre::Vector3()); // should only happen if the plane is too small + } + + std::pair MouseState::terrainUnderCursor(const int mouseX, const int mouseY) + { + if(!getViewport()) + return std::make_pair("", Ogre::Vector3()); + + float x = (float) mouseX / getViewport()->getActualWidth(); + float y = (float) mouseY / getViewport()->getActualHeight(); + + std::pair result = mPhysics->castRay(x, y, mSceneManager, getCamera()); + if(result.first != "") + { + // FIXME: is there a better way to distinguish terrain from objects? + QString name = QString(result.first.c_str()); + if(name.contains(QRegExp("^HeightField"))) + { + return result; + } + } + + return std::make_pair("", Ogre::Vector3()); + } + + std::pair MouseState::objectUnderCursor(const int mouseX, const int mouseY) + { + if(!getViewport()) + return std::make_pair("", Ogre::Vector3()); + + float x = (float) mouseX / getViewport()->getActualWidth(); + float y = (float) mouseY / getViewport()->getActualHeight(); + + std::pair result = mPhysics->castRay(x, y, mSceneManager, getCamera()); + if(result.first != "") + { + // NOTE: anything not terrain is assumed to be an object + QString name = QString(result.first.c_str()); + if(!name.contains(QRegExp("^HeightField"))) + { + uint32_t visibilityMask = getViewport()->getVisibilityMask(); + bool ignoreObjects = !(visibilityMask & (uint32_t)CSVRender::Element_Reference); + + if(!ignoreObjects && mSceneManager->hasSceneNode(result.first)) + { + return result; + } + } + } + + return std::make_pair("", Ogre::Vector3()); + } + + void MouseState::updateSceneWidgets() + { + std::map sceneWidgets = mPhysics->sceneWidgets(); + + std::map::iterator iter = sceneWidgets.begin(); + for(; iter != sceneWidgets.end(); ++iter) + { + (*iter).second->updateScene(); + } + } + + Ogre::Camera *MouseState::getCamera() + { + return mParent->getCamera(); + } + + Ogre::Viewport *MouseState::getViewport() + { + return mParent->getCamera()->getViewport(); + } +} diff --git a/apps/opencs/view/render/mousestate.hpp b/apps/opencs/view/render/mousestate.hpp new file mode 100644 index 000000000..70e18427f --- /dev/null +++ b/apps/opencs/view/render/mousestate.hpp @@ -0,0 +1,91 @@ +#ifndef OPENCS_VIEW_MOUSESTATE_H +#define OPENCS_VIEW_MOUSESTATE_H + +#include +#include +#include +#include + +class QElapsedTimer; +class QMouseEvent; +class QWheelEvent; + +namespace Ogre +{ + class Plane; + class SceneManager; + class Camera; + class Viewport; +} + +namespace CSVWorld +{ + class PhysicsSystem; +} + +namespace CSMWorld +{ + class IdTable; +} + +namespace CSVRender +{ + class WorldspaceWidget; + + class MouseState + { + enum MouseStates + { + Mouse_Grab, + Mouse_Drag, + Mouse_Edit, + Mouse_Default + }; + MouseStates mMouseState; + + WorldspaceWidget *mParent; + boost::shared_ptr mPhysics; + Ogre::SceneManager *mSceneManager; // local copy + + QPoint mOldPos; + std::string mCurrentObj; + std::string mGrabbedSceneNode; + QElapsedTimer *mMouseEventTimer; + Ogre::Plane *mPlane; + Ogre::Vector3 mOrigObjPos; + Ogre::Vector3 mOrigMousePos; + Ogre::Vector3 mCurrentMousePos; + float mOffset; + + CSMWorld::IdTable *mIdTableModel; + int mColIndexPosX; + int mColIndexPosY; + int mColIndexPosZ; + + public: + + MouseState(WorldspaceWidget *parent); + ~MouseState(); + + void mouseMoveEvent (QMouseEvent *event); + void mousePressEvent (QMouseEvent *event); + void mouseReleaseEvent (QMouseEvent *event); + void mouseDoubleClickEvent (QMouseEvent *event); + bool wheelEvent (QWheelEvent *event); + + void cancelDrag(); + + private: + + std::pair mousePositionOnPlane(const QPoint &pos, const Ogre::Plane &plane); + std::pair terrainUnderCursor(const int mouseX, const int mouseY); + std::pair objectUnderCursor(const int mouseX, const int mouseY); + std::pair planeAxis(); + void updateSceneWidgets(); + + Ogre::Camera *getCamera(); // friend access + Ogre::Viewport *getViewport(); // friend access + }; +} + +#endif // OPENCS_VIEW_MOUSESTATE_H diff --git a/apps/opencs/view/render/navigation.cpp b/apps/opencs/view/render/navigation.cpp index 14ae7f0b7..705759104 100644 --- a/apps/opencs/view/render/navigation.cpp +++ b/apps/opencs/view/render/navigation.cpp @@ -11,9 +11,14 @@ float CSVRender::Navigation::getFactor (bool mouse) const return factor; } +CSVRender::Navigation::Navigation() + : mFastModeFactor(1) +{ +} + CSVRender::Navigation::~Navigation() {} void CSVRender::Navigation::setFastModeFactor (float factor) { mFastModeFactor = factor; -} \ No newline at end of file +} diff --git a/apps/opencs/view/render/navigation.hpp b/apps/opencs/view/render/navigation.hpp index 873519609..ead8f3e13 100644 --- a/apps/opencs/view/render/navigation.hpp +++ b/apps/opencs/view/render/navigation.hpp @@ -20,6 +20,7 @@ namespace CSVRender public: + Navigation(); virtual ~Navigation(); void setFastModeFactor (float factor); diff --git a/apps/opencs/view/render/navigation1st.cpp b/apps/opencs/view/render/navigation1st.cpp index 91f88634a..5d9a03468 100644 --- a/apps/opencs/view/render/navigation1st.cpp +++ b/apps/opencs/view/render/navigation1st.cpp @@ -44,10 +44,11 @@ bool CSVRender::Navigation1st::mouseMoved (const QPoint& delta, int mode) float deltaPitch = getFactor (true) * delta.y(); Ogre::Radian newPitch = oldPitch + Ogre::Degree (deltaPitch); - Ogre::Radian limit (Ogre::Math::PI/2-0.5); - - if ((deltaPitch>0 && newPitch-limit)) + if ((deltaPitch>0 && newPitchOgre::Radian(0.5))) + { mCamera->pitch (Ogre::Degree (deltaPitch)); + } } return true; diff --git a/apps/opencs/view/render/object.cpp b/apps/opencs/view/render/object.cpp index bb7c2f386..3607fb415 100644 --- a/apps/opencs/view/render/object.cpp +++ b/apps/opencs/view/render/object.cpp @@ -9,6 +9,10 @@ #include "../../model/world/ref.hpp" #include "../../model/world/refidcollection.hpp" +#include "../world/physicssystem.hpp" + +#include "elements.hpp" + void CSVRender::Object::clearSceneNode (Ogre::SceneNode *node) { for (Ogre::SceneNode::ObjectIterator iter = node->getAttachedObjectIterator(); @@ -31,11 +35,15 @@ void CSVRender::Object::clear() { mObject.setNull(); - clearSceneNode (mBase); + if (mBase) + clearSceneNode (mBase); } void CSVRender::Object::update() { + if(!mObject.isNull()) + mPhysics->removePhysicsObject(mBase->getName()); + clear(); std::string model; @@ -63,12 +71,31 @@ void CSVRender::Object::update() { Ogre::Entity* entity = mBase->getCreator()->createEntity (Ogre::SceneManager::PT_CUBE); entity->setMaterialName("BaseWhite"); /// \todo adjust material according to error + entity->setVisibilityFlags (Element_Reference); mBase->attachObject (entity); } else { mObject = NifOgre::Loader::createObjects (mBase, "Meshes\\" + model); + mObject->setVisibilityFlags (Element_Reference); + + if (mPhysics && !mReferenceId.empty()) + { + const CSMWorld::CellRef& reference = getReference(); + + // position + Ogre::Vector3 position; + if (!mForceBaseToZero) + position = Ogre::Vector3(reference.mPos.pos[0], reference.mPos.pos[1], reference.mPos.pos[2]); + + // orientation + Ogre::Quaternion xr (Ogre::Radian (-reference.mPos.rot[0]), Ogre::Vector3::UNIT_X); + Ogre::Quaternion yr (Ogre::Radian (-reference.mPos.rot[1]), Ogre::Vector3::UNIT_Y); + Ogre::Quaternion zr (Ogre::Radian (-reference.mPos.rot[2]), Ogre::Vector3::UNIT_Z); + + mPhysics->addObject("meshes\\" + model, mBase->getName(), mReferenceId, reference.mScale, position, xr*yr*zr); + } } } @@ -106,8 +133,9 @@ const CSMWorld::CellRef& CSVRender::Object::getReference() const } CSVRender::Object::Object (const CSMWorld::Data& data, Ogre::SceneNode *cellNode, - const std::string& id, bool referenceable, bool forceBaseToZero) -: mData (data), mBase (0), mForceBaseToZero (forceBaseToZero) + const std::string& id, bool referenceable, boost::shared_ptr physics, + bool forceBaseToZero) +: mData (data), mBase (0), mForceBaseToZero (forceBaseToZero), mPhysics(physics) { mBase = cellNode->createChildSceneNode(); @@ -130,7 +158,12 @@ CSVRender::Object::~Object() clear(); if (mBase) + { + if(mPhysics) // preview may not have physics enabled + mPhysics->removeObject(mBase->getName()); + mBase->getCreator()->destroySceneNode (mBase); + } } bool CSVRender::Object::referenceableDataChanged (const QModelIndex& topLeft, @@ -210,4 +243,4 @@ std::string CSVRender::Object::getReferenceId() const std::string CSVRender::Object::getReferenceableId() const { return mReferenceableId; -} \ No newline at end of file +} diff --git a/apps/opencs/view/render/object.hpp b/apps/opencs/view/render/object.hpp index df39d7393..3ed4fa793 100644 --- a/apps/opencs/view/render/object.hpp +++ b/apps/opencs/view/render/object.hpp @@ -1,7 +1,11 @@ #ifndef OPENCS_VIEW_OBJECT_H #define OPENCS_VIEW_OBJECT_H +#include + +#ifndef Q_MOC_RUN #include +#endif class QModelIndex; @@ -16,6 +20,11 @@ namespace CSMWorld class CellRef; } +namespace CSVWorld +{ + class PhysicsSystem; +} + namespace CSVRender { class Object @@ -26,6 +35,7 @@ namespace CSVRender Ogre::SceneNode *mBase; NifOgre::ObjectScenePtr mObject; bool mForceBaseToZero; + boost::shared_ptr mPhysics; /// Not implemented Object (const Object&); @@ -51,7 +61,9 @@ namespace CSVRender public: Object (const CSMWorld::Data& data, Ogre::SceneNode *cellNode, - const std::string& id, bool referenceable, bool forceBaseToZero = false); + const std::string& id, bool referenceable, + boost::shared_ptr physics = boost::shared_ptr (), + bool forceBaseToZero = false); /// \param forceBaseToZero If this is a reference ignore the coordinates and place /// it at 0, 0, 0 instead. @@ -77,4 +89,4 @@ namespace CSVRender }; } -#endif \ No newline at end of file +#endif diff --git a/apps/opencs/view/render/overlaymask.cpp b/apps/opencs/view/render/overlaymask.cpp new file mode 100644 index 000000000..09f020354 --- /dev/null +++ b/apps/opencs/view/render/overlaymask.cpp @@ -0,0 +1,52 @@ +#include "overlaymask.hpp" + +#include +#include + +#include "textoverlay.hpp" +#include "../../model/world/cellcoordinates.hpp" + +namespace CSVRender +{ + +// ideas from http://www.ogre3d.org/forums/viewtopic.php?f=5&t=44828#p486334 +OverlayMask::OverlayMask(std::map &overlays, Ogre::Viewport* viewport) + : mTextOverlays(overlays), mViewport(viewport) +{ +} + +OverlayMask::~OverlayMask() +{ +} + +void OverlayMask::setViewport(Ogre::Viewport *viewport) +{ + mViewport = viewport; +} + +void OverlayMask::preViewportUpdate(const Ogre::RenderTargetViewportEvent &event) +{ + if(event.source == mViewport) + { + Ogre::OverlayManager &overlayMgr = Ogre::OverlayManager::getSingleton(); + for(Ogre::OverlayManager::OverlayMapIterator iter = overlayMgr.getOverlayIterator(); + iter.hasMoreElements();) + { + Ogre::Overlay* item = iter.getNext(); + for(Ogre::Overlay::Overlay2DElementsIterator it = item->get2DElementsIterator(); + it.hasMoreElements();) + { + Ogre::OverlayContainer* container = it.getNext(); + if(container) container->hide(); + } + } + + std::map::iterator it = mTextOverlays.begin(); + for(; it != mTextOverlays.end(); ++it) + { + it->second->show(true); + } + } +} + +} diff --git a/apps/opencs/view/render/overlaymask.hpp b/apps/opencs/view/render/overlaymask.hpp new file mode 100644 index 000000000..ec050cac4 --- /dev/null +++ b/apps/opencs/view/render/overlaymask.hpp @@ -0,0 +1,42 @@ +#ifndef OPENCS_VIEW_OVERLAYMASK_H +#define OPENCS_VIEW_OVERLAYMASK_H + +#include + +namespace Ogre +{ + class Viewport; + class RendertargetViewportEvent; +} + +namespace CSMWorld +{ + class CellCoordinates; +} + +namespace CSVRender +{ + class TextOverlay; + + class OverlayMask : public Ogre::RenderTargetListener + { + + std::map &mTextOverlays; + Ogre::Viewport* mViewport; + + public: + + OverlayMask(std::map &overlays, + Ogre::Viewport* viewport); + + virtual ~OverlayMask(); + + void setViewport(Ogre::Viewport *viewport); + + protected: + + virtual void preViewportUpdate(const Ogre::RenderTargetViewportEvent &event); + }; +} + +#endif // OPENCS_VIEW_OVERLAYMASK_H diff --git a/apps/opencs/view/render/overlaysystem.cpp b/apps/opencs/view/render/overlaysystem.cpp new file mode 100644 index 000000000..f565f5af0 --- /dev/null +++ b/apps/opencs/view/render/overlaysystem.cpp @@ -0,0 +1,34 @@ +#include "overlaysystem.hpp" + +#include + +#include + +namespace CSVRender +{ + OverlaySystem *OverlaySystem::mOverlaySystemInstance = 0; + + OverlaySystem::OverlaySystem() + { + assert(!mOverlaySystemInstance); + mOverlaySystemInstance = this; + mOverlaySystem = new Ogre::OverlaySystem(); + } + + OverlaySystem::~OverlaySystem() + { + delete mOverlaySystem; + } + + OverlaySystem &OverlaySystem::instance() + { + assert(mOverlaySystemInstance); + return *mOverlaySystemInstance; + } + + Ogre::OverlaySystem *OverlaySystem::get() + { + return mOverlaySystem; + } +} + diff --git a/apps/opencs/view/render/overlaysystem.hpp b/apps/opencs/view/render/overlaysystem.hpp new file mode 100644 index 000000000..f8a78f329 --- /dev/null +++ b/apps/opencs/view/render/overlaysystem.hpp @@ -0,0 +1,26 @@ +#ifndef OPENCS_VIEW_OVERLAYSYSTEM_H +#define OPENCS_VIEW_OVERLAYSYSTEM_H + +namespace Ogre +{ + class OverlaySystem; +} + +namespace CSVRender +{ + class OverlaySystem + { + Ogre::OverlaySystem *mOverlaySystem; + static OverlaySystem *mOverlaySystemInstance; + + public: + + OverlaySystem(); + ~OverlaySystem(); + static OverlaySystem &instance(); + + Ogre::OverlaySystem *get(); + }; +} + +#endif // OPENCS_VIEW_OVERLAYSYSTEM_H diff --git a/apps/opencs/view/render/pagedworldspacewidget.cpp b/apps/opencs/view/render/pagedworldspacewidget.cpp index 1ee32fa97..cf9edb548 100644 --- a/apps/opencs/view/render/pagedworldspacewidget.cpp +++ b/apps/opencs/view/render/pagedworldspacewidget.cpp @@ -3,13 +3,30 @@ #include +#include + #include +#include +#include +#include +#include +#include +#include -#include +#include +#include "textoverlay.hpp" +#include "overlaymask.hpp" #include "../../model/world/tablemimedata.hpp" #include "../../model/world/idtable.hpp" +#include "../widget/scenetooltoggle.hpp" +#include "../widget/scenetoolmode.hpp" +#include "../widget/scenetooltoggle2.hpp" + +#include "editmode.hpp" +#include "elements.hpp" + bool CSVRender::PagedWorldspaceWidget::adjustCells() { bool modified = false; @@ -18,7 +35,7 @@ bool CSVRender::PagedWorldspaceWidget::adjustCells() const CSMWorld::IdCollection& cells = mDocument.getData().getCells(); { - // remove + // remove (or name/region modified) std::map::iterator iter (mCells.begin()); while (iter!=mCells.end()) @@ -28,12 +45,57 @@ bool CSVRender::PagedWorldspaceWidget::adjustCells() if (!mSelection.has (iter->first) || index==-1 || cells.getRecord (index).mState==CSMWorld::RecordBase::State_Deleted) { + // delete overlays + std::map::iterator itOverlay = mTextOverlays.find(iter->first); + if(itOverlay != mTextOverlays.end()) + { + delete itOverlay->second; + mTextOverlays.erase(itOverlay); + } + + // destroy manual objects + getSceneManager()->destroyManualObject("manual"+iter->first.getId(mWorldspace)); + delete iter->second; mCells.erase (iter++); + modified = true; } else + { + // check if name or region field has changed + // FIXME: config setting + std::string name = cells.getRecord(index).get().mName; + std::string region = cells.getRecord(index).get().mRegion; + + std::map::iterator it = mTextOverlays.find(iter->first); + if(it != mTextOverlays.end()) + { + if(it->second->getDesc() != "") // previously had name + { + if(name != it->second->getDesc()) // new name + { + if(name != "") + it->second->setDesc(name); + else // name deleted, use region + it->second->setDesc(region); + it->second->update(); + } + } + else if(name != "") // name added + { + it->second->setDesc(name); + it->second->update(); + } + else if(region != it->second->getDesc()) // new region + { + it->second->setDesc(region); + it->second->update(); + } + modified = true; + } ++iter; + } } } @@ -46,18 +108,63 @@ bool CSVRender::PagedWorldspaceWidget::adjustCells() { int index = cells.searchId (iter->getId (mWorldspace)); - if (index!=0 && cells.getRecord (index).mState!=CSMWorld::RecordBase::State_Deleted && + if (index > 0 && cells.getRecord (index).mState!=CSMWorld::RecordBase::State_Deleted && mCells.find (*iter)==mCells.end()) { + Cell *cell = new Cell (mDocument.getData(), getSceneManager(), + iter->getId (mWorldspace), mDocument.getPhysics()); + mCells.insert (std::make_pair (*iter, cell)); + + float height = cell->getTerrainHeightAt(Ogre::Vector3( + ESM::Land::REAL_SIZE * iter->getX() + ESM::Land::REAL_SIZE/2, + ESM::Land::REAL_SIZE * iter->getY() + ESM::Land::REAL_SIZE/2, + 0)); if (setCamera) { setCamera = false; - getCamera()->setPosition (8192*iter->getX()+4096, 8192*iter->getY()+4096, 0); + getCamera()->setPosition ( + ESM::Land::REAL_SIZE * iter->getX() + ESM::Land::REAL_SIZE/2, + ESM::Land::REAL_SIZE * iter->getY() + ESM::Land::REAL_SIZE/2, + height); + // better camera position at the start + getCamera()->move(getCamera()->getDirection() * -6000); // FIXME: config setting } - mCells.insert (std::make_pair (*iter, - new Cell (mDocument.getData(), getSceneManager(), - iter->getId (mWorldspace)))); + Ogre::ManualObject* manual = + getSceneManager()->createManualObject("manual" + iter->getId(mWorldspace)); + manual->begin("BaseWhite", Ogre::RenderOperation::OT_LINE_LIST); + // define start and end point (x, y, z) + manual-> position(ESM::Land::REAL_SIZE * iter->getX() + ESM::Land::REAL_SIZE/2, + ESM::Land::REAL_SIZE * iter->getY() + ESM::Land::REAL_SIZE/2, + height); + manual-> position(ESM::Land::REAL_SIZE * iter->getX() + ESM::Land::REAL_SIZE/2, + ESM::Land::REAL_SIZE * iter->getY() + ESM::Land::REAL_SIZE/2, + height+200); // FIXME: config setting + manual->end(); + manual->setBoundingBox(Ogre::AxisAlignedBox( + ESM::Land::REAL_SIZE * iter->getX() + ESM::Land::REAL_SIZE/2, + ESM::Land::REAL_SIZE * iter->getY() + ESM::Land::REAL_SIZE/2, + height, + ESM::Land::REAL_SIZE * iter->getX() + ESM::Land::REAL_SIZE/2, + ESM::Land::REAL_SIZE * iter->getY() + ESM::Land::REAL_SIZE/2, + height+200)); + getSceneManager()->getRootSceneNode()->createChildSceneNode()->attachObject(manual); + manual->setVisible(false); + + CSVRender::TextOverlay *textDisp = + new CSVRender::TextOverlay(manual, getCamera(), iter->getId(mWorldspace)); + textDisp->enable(true); + textDisp->setCaption(iter->getId(mWorldspace)); + std::string desc = cells.getRecord(index).get().mName; + if(desc == "") desc = cells.getRecord(index).get().mRegion; + textDisp->setDesc(desc); // FIXME: config setting + textDisp->update(); + mTextOverlays.insert(std::make_pair(*iter, textDisp)); + if(!mOverlayMask) + { + mOverlayMask = new OverlayMask(mTextOverlays, getViewport()); + addRenderTargetListener(mOverlayMask); + } modified = true; } @@ -66,6 +173,96 @@ bool CSVRender::PagedWorldspaceWidget::adjustCells() return modified; } +void CSVRender::PagedWorldspaceWidget::mousePressEvent (QMouseEvent *event) +{ + if(event->button() == Qt::RightButton) + { + std::map::iterator iter = mTextOverlays.begin(); + for(; iter != mTextOverlays.end(); ++iter) + { + if(mDisplayCellCoord && + iter->second->isEnabled() && iter->second->container().contains(event->x(), event->y())) + { + return; + } + } + } + WorldspaceWidget::mousePressEvent(event); +} + +void CSVRender::PagedWorldspaceWidget::mouseReleaseEvent (QMouseEvent *event) +{ + if(event->button() == Qt::RightButton) + { + std::map::iterator iter = mTextOverlays.begin(); + for(; iter != mTextOverlays.end(); ++iter) + { + if(mDisplayCellCoord && + iter->second->isEnabled() && iter->second->container().contains(event->x(), event->y())) + { + std::cout << "clicked: " << iter->second->getCaption() << std::endl; + return; + } + } + } + WorldspaceWidget::mouseReleaseEvent(event); +} + +void CSVRender::PagedWorldspaceWidget::mouseDoubleClickEvent (QMouseEvent *event) +{ + WorldspaceWidget::mouseDoubleClickEvent(event); +} + +void CSVRender::PagedWorldspaceWidget::addVisibilitySelectorButtons ( + CSVWidget::SceneToolToggle2 *tool) +{ + WorldspaceWidget::addVisibilitySelectorButtons (tool); + tool->addButton (Element_Terrain, "Terrain"); + tool->addButton (Element_Fog, "Fog", "", true); +} + +void CSVRender::PagedWorldspaceWidget::addEditModeSelectorButtons ( + CSVWidget::SceneToolMode *tool) +{ + WorldspaceWidget::addEditModeSelectorButtons (tool); + + /// \todo replace EditMode with suitable subclasses + tool->addButton ( + new EditMode (this, QIcon (":placeholder"), Element_Reference, "Terrain shape editing"), + "terrain-shape"); + tool->addButton ( + new EditMode (this, QIcon (":placeholder"), Element_Reference, "Terrain texture editing"), + "terrain-texture"); + tool->addButton ( + new EditMode (this, QIcon (":placeholder"), Element_Reference, "Terrain vertex paint editing"), + "terrain-vertex"); + tool->addButton ( + new EditMode (this, QIcon (":placeholder"), Element_Reference, "Terrain movement"), + "terrain-move"); +} + +void CSVRender::PagedWorldspaceWidget::updateOverlay() +{ + if(getCamera()->getViewport()) + { + if((uint32_t)getCamera()->getViewport()->getVisibilityMask() + & (uint32_t)CSVRender::Element_CellMarker) + mDisplayCellCoord = true; + else + mDisplayCellCoord = false; + } + + if(!mTextOverlays.empty()) + { + std::map::iterator it = mTextOverlays.begin(); + for(; it != mTextOverlays.end(); ++it) + { + it->second->enable(mDisplayCellCoord); + it->second->update(); + } + } +} + void CSVRender::PagedWorldspaceWidget::referenceableDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) { @@ -129,8 +326,23 @@ void CSVRender::PagedWorldspaceWidget::referenceAdded (const QModelIndex& parent flagAsModified(); } +std::string CSVRender::PagedWorldspaceWidget::getStartupInstruction() +{ + Ogre::Vector3 position = getCamera()->getPosition(); + + std::ostringstream stream; + + stream + << "player->position " + << position.x << ", " << position.y << ", " << position.z + << ", 0"; + + return stream.str(); +} + CSVRender::PagedWorldspaceWidget::PagedWorldspaceWidget (QWidget* parent, CSMDoc::Document& document) -: WorldspaceWidget (document, parent), mDocument (document), mWorldspace ("std::default") +: WorldspaceWidget (document, parent), mDocument (document), mWorldspace ("std::default"), + mControlElements(NULL), mDisplayCellCoord(true), mOverlayMask(NULL) { QAbstractItemModel *cells = document.getData().getTableModel (CSMWorld::UniversalId::Type_Cells); @@ -147,7 +359,23 @@ CSVRender::PagedWorldspaceWidget::~PagedWorldspaceWidget() { for (std::map::iterator iter (mCells.begin()); iter!=mCells.end(); ++iter) + { + delete iter->second; + + getSceneManager()->destroyManualObject("manual"+iter->first.getId(mWorldspace)); + } + + for (std::map::iterator iter (mTextOverlays.begin()); + iter != mTextOverlays.end(); ++iter) + { delete iter->second; + } + + if(mOverlayMask) + { + removeRenderTargetListener(mOverlayMask); + delete mOverlayMask; + } } void CSVRender::PagedWorldspaceWidget::useViewHint (const std::string& hint) @@ -202,8 +430,15 @@ std::pair< int, int > CSVRender::PagedWorldspaceWidget::getCoordinatesFromId (co return std::make_pair(x, y); } -void CSVRender::PagedWorldspaceWidget::handleDrop (const std::vector< CSMWorld::UniversalId >& data) +bool CSVRender::PagedWorldspaceWidget::handleDrop ( + const std::vector< CSMWorld::UniversalId >& data, DropType type) { + if (WorldspaceWidget::handleDrop (data, type)) + return true; + + if (type!=Type_CellsExterior) + return false; + bool selectionChanged = false; for (unsigned i = 0; i < data.size(); ++i) { @@ -220,16 +455,23 @@ void CSVRender::PagedWorldspaceWidget::handleDrop (const std::vector< CSMWorld:: emit cellSelectionChanged(mSelection); } + + return true; } -CSVRender::WorldspaceWidget::dropRequirments CSVRender::PagedWorldspaceWidget::getDropRequirements (CSVRender::WorldspaceWidget::dropType type) const +CSVRender::WorldspaceWidget::dropRequirments CSVRender::PagedWorldspaceWidget::getDropRequirements (CSVRender::WorldspaceWidget::DropType type) const { + dropRequirments requirements = WorldspaceWidget::getDropRequirements (type); + + if (requirements!=ignored) + return requirements; + switch (type) { - case cellsExterior: + case Type_CellsExterior: return canHandle; - case cellsInterior: + case Type_CellsInterior: return needUnpaged; default: @@ -237,6 +479,30 @@ CSVRender::WorldspaceWidget::dropRequirments CSVRender::PagedWorldspaceWidget::g } } +unsigned int CSVRender::PagedWorldspaceWidget::getVisibilityMask() const +{ + return WorldspaceWidget::getVisibilityMask() | mControlElements->getSelection(); +} + +CSVWidget::SceneToolToggle *CSVRender::PagedWorldspaceWidget::makeControlVisibilitySelector ( + CSVWidget::SceneToolbar *parent) +{ + mControlElements = new CSVWidget::SceneToolToggle (parent, + "Controls & Guides Visibility", ":placeholder"); + + mControlElements->addButton (":placeholder", Element_CellMarker, ":placeholder", + "Cell marker"); + mControlElements->addButton (":placeholder", Element_CellArrow, ":placeholder", "Cell arrows"); + mControlElements->addButton (":placeholder", Element_CellBorder, ":placeholder", "Cell border"); + + mControlElements->setSelection (0xffffffff); + + connect (mControlElements, SIGNAL (selectionChanged()), + this, SLOT (elementSelectionChanged())); + + return mControlElements; +} + void CSVRender::PagedWorldspaceWidget::cellDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) { @@ -258,4 +524,4 @@ void CSVRender::PagedWorldspaceWidget::cellAdded (const QModelIndex& index, int /// \todo check if no selected cell is affected and do not update, if that is the case if (adjustCells()) flagAsModified(); -} \ No newline at end of file +} diff --git a/apps/opencs/view/render/pagedworldspacewidget.hpp b/apps/opencs/view/render/pagedworldspacewidget.hpp index c4fb789ee..3db6ee4ed 100644 --- a/apps/opencs/view/render/pagedworldspacewidget.hpp +++ b/apps/opencs/view/render/pagedworldspacewidget.hpp @@ -8,8 +8,16 @@ #include "worldspacewidget.hpp" #include "cell.hpp" +namespace CSVWidget +{ + class SceneToolToggle; +} + namespace CSVRender { + class TextOverlay; + class OverlayMask; + class PagedWorldspaceWidget : public WorldspaceWidget { Q_OBJECT @@ -18,6 +26,10 @@ namespace CSVRender CSMWorld::CellSelection mSelection; std::map mCells; std::string mWorldspace; + CSVWidget::SceneToolToggle *mControlElements; + bool mDisplayCellCoord; + std::map mTextOverlays; + OverlayMask *mOverlayMask; private: @@ -41,6 +53,8 @@ namespace CSVRender virtual void referenceAdded (const QModelIndex& index, int start, int end); + virtual std::string getStartupInstruction(); + public: PagedWorldspaceWidget (QWidget *parent, CSMDoc::Document& document); @@ -52,11 +66,34 @@ namespace CSVRender void useViewHint (const std::string& hint); - void setCellSelection (const CSMWorld::CellSelection& selection); + void setCellSelection(const CSMWorld::CellSelection& selection); + + /// \return Drop handled? + virtual bool handleDrop (const std::vector& data, + DropType type); + + virtual dropRequirments getDropRequirements(DropType type) const; + + /// \attention The created tool is not added to the toolbar (via addTool). Doing + /// that is the responsibility of the calling function. + virtual CSVWidget::SceneToolToggle *makeControlVisibilitySelector ( + CSVWidget::SceneToolbar *parent); + + virtual unsigned int getVisibilityMask() const; + + protected: + + virtual void addVisibilitySelectorButtons (CSVWidget::SceneToolToggle2 *tool); + + virtual void addEditModeSelectorButtons (CSVWidget::SceneToolMode *tool); + + virtual void updateOverlay(); + + virtual void mousePressEvent (QMouseEvent *event); - virtual void handleDrop(const std::vector& data); + virtual void mouseReleaseEvent (QMouseEvent *event); - virtual dropRequirments getDropRequirements(dropType type) const; + virtual void mouseDoubleClickEvent (QMouseEvent *event); signals: diff --git a/apps/opencs/view/render/previewwidget.cpp b/apps/opencs/view/render/previewwidget.cpp index 75b4e9396..da18e7c89 100644 --- a/apps/opencs/view/render/previewwidget.cpp +++ b/apps/opencs/view/render/previewwidget.cpp @@ -10,7 +10,7 @@ CSVRender::PreviewWidget::PreviewWidget (CSMWorld::Data& data, const std::string& id, bool referenceable, QWidget *parent) : SceneWidget (parent), mData (data), - mObject (data, getSceneManager()->getRootSceneNode(), id, referenceable, true) + mObject (data, getSceneManager()->getRootSceneNode(), id, referenceable, boost::shared_ptr(), true) { setNavigation (&mOrbit); diff --git a/apps/opencs/view/render/scenewidget.cpp b/apps/opencs/view/render/scenewidget.cpp index ebd3eb764..55cf039fc 100644 --- a/apps/opencs/view/render/scenewidget.cpp +++ b/apps/opencs/view/render/scenewidget.cpp @@ -11,23 +11,28 @@ #include #include #include +#include #include "../widget/scenetoolmode.hpp" +#include "../../model/settings/usersettings.hpp" #include "navigation.hpp" #include "lighting.hpp" +#include "overlaysystem.hpp" namespace CSVRender { SceneWidget::SceneWidget(QWidget *parent) : QWidget(parent) - , mWindow(NULL) , mCamera(NULL) - , mSceneMgr(NULL), mNavigation (0), mLighting (0), mUpdate (false) - , mKeyForward (false), mKeyBackward (false), mKeyLeft (false), mKeyRight (false) + , mSceneMgr(NULL) + , mWindow(NULL) + , mViewport(NULL) + , mNavigation (0), mLighting (0), mUpdate (false), mKeyForward (false) + , mKeyBackward (false), mKeyLeft (false), mKeyRight (false) , mKeyRollLeft (false), mKeyRollRight (false) , mFast (false), mDragging (false), mMod1 (false) - , mFastFactor (4) /// \todo make this configurable + , mFastFactor (4) , mDefaultAmbient (0, 0, 0, 0), mHasDefaultAmbient (false) { setAttribute(Qt::WA_PaintOnScreen); @@ -44,15 +49,27 @@ namespace CSVRender mCamera->setPosition (300, 0, 0); mCamera->lookAt (0, 0, 0); mCamera->setNearClipDistance (0.1); - mCamera->setFarClipDistance (300000); ///< \todo make this configurable + + CSMSettings::UserSettings &userSettings = CSMSettings::UserSettings::instance(); + + float farClipDist = userSettings.setting("3d-render/far-clip-distance", QString("300000")).toFloat(); + mCamera->setFarClipDistance (farClipDist); + + mFastFactor = userSettings.setting("scene-input/fast-factor", QString("4")).toInt(); + mCamera->roll (Ogre::Degree (90)); setLighting (&mLightingDay); + mOverlaySystem = OverlaySystem::instance().get(); + mSceneMgr->addRenderQueueListener(mOverlaySystem); + QTimer *timer = new QTimer (this); connect (timer, SIGNAL (timeout()), this, SLOT (update())); - timer->start (20); ///< \todo make this configurable + + int timerStart = userSettings.setting("scene-input/timer", QString("20")).toInt(); + timer->start (timerStart); /// \todo make shortcut configurable QShortcut *focusToolbar = new QShortcut (Qt::Key_T, this, 0, 0, Qt::WidgetWithChildrenShortcut); @@ -64,19 +81,19 @@ namespace CSVRender CSVWidget::SceneToolMode *tool = new CSVWidget::SceneToolMode (parent, "Lighting Mode"); /// \todo replace icons - tool->addButton (":door.png", "day", + tool->addButton (":scenetoolbar/day", "day", "Day" "

  • Cell specific ambient in interiors
  • " "
  • Low ambient in exteriors
  • " "
  • Strong directional light source/lir>" "
  • This mode closely resembles day time in-game
"); - tool->addButton (":GMST.png", "night", + tool->addButton (":scenetoolbar/night", "night", "Night" "
  • Cell specific ambient in interiors
  • " "
  • Low ambient in exteriors
  • " "
  • Weak directional light source
  • " "
  • This mode closely resembles night time in-game
"); - tool->addButton (":Info.png", "bright", + tool->addButton (":scenetoolbar/bright", "bright", "Bright" "
  • Maximum ambient
  • " "
  • Strong directional light source
"); @@ -118,7 +135,16 @@ namespace CSVRender params.insert(std::make_pair("externalWindowHandle", windowHandle.str())); params.insert(std::make_pair("title", windowTitle.str())); - params.insert(std::make_pair("FSAA", "0")); // TODO setting + + std::string antialiasing = + CSMSettings::UserSettings::instance().settingValue("3d-render/antialiasing").toStdString(); + if(antialiasing == "MSAA 16") antialiasing = "16"; + else if(antialiasing == "MSAA 8") antialiasing = "8"; + else if(antialiasing == "MSAA 4") antialiasing = "4"; + else if(antialiasing == "MSAA 2") antialiasing = "2"; + else antialiasing = "0"; + params.insert(std::make_pair("FSAA", antialiasing)); + params.insert(std::make_pair("vsync", "false")); // TODO setting #if OGRE_PLATFORM == OGRE_PLATFORM_APPLE params.insert(std::make_pair("macAPI", "cocoa")); @@ -126,7 +152,9 @@ namespace CSVRender #endif mWindow = Ogre::Root::getSingleton().createRenderWindow(windowTitle.str(), this->width(), this->height(), false, ¶ms); - mWindow->addViewport(mCamera)->setBackgroundColour(Ogre::ColourValue(0.3,0.3,0.3,1)); + + mViewport = mWindow->addViewport (mCamera); + mViewport->setBackgroundColour (Ogre::ColourValue (0.3,0.3,0.3,1)); Ogre::Real aspectRatio = Ogre::Real(width()) / Ogre::Real(height()); mCamera->setAspectRatio(aspectRatio); @@ -137,8 +165,17 @@ namespace CSVRender if (mWindow) Ogre::Root::getSingleton().destroyRenderTarget (mWindow); + if (mSceneMgr) + mSceneMgr->removeRenderQueueListener (mOverlaySystem); + if (mSceneMgr) Ogre::Root::getSingleton().destroySceneManager (mSceneMgr); + + } + + void SceneWidget::setVisibilityMask (unsigned int mask) + { + mViewport->setVisibilityMask (mask); } void SceneWidget::setNavigation (Navigation *navigation) @@ -151,6 +188,24 @@ namespace CSVRender } } + void SceneWidget::addRenderTargetListener(Ogre::RenderTargetListener *listener) + { + mWindow->addListener(listener); + } + + void SceneWidget::removeRenderTargetListener(Ogre::RenderTargetListener *listener) + { + mWindow->removeListener(listener); + } + + Ogre::Viewport *SceneWidget::getViewport() + { + if (!mWindow) + updateOgreWindow(); + + return mViewport; + } + Ogre::SceneManager *SceneWidget::getSceneManager() { return mSceneMgr; @@ -351,14 +406,18 @@ namespace CSVRender { mUpdate = false; mWindow->update(); + updateOverlay(); } } - int SceneWidget::getFastFactor() const + void SceneWidget::updateScene() { - return mFast ? mFastFactor : 1; + flagAsModified(); } + void SceneWidget::updateOverlay() + { } + void SceneWidget::setLighting (Lighting *lighting) { if (mLighting) @@ -380,4 +439,32 @@ namespace CSVRender else if (mode=="bright") setLighting (&mLightingBright); } + + void SceneWidget::updateUserSetting (const QString &key, const QStringList &list) + { + if(key.contains(QRegExp("^\\b(Objects|Shader|Scene)", Qt::CaseInsensitive))) + flagAsModified(); + + if(key == "3d-render/far-clip-distance" && !list.empty()) + { + if(mCamera->getFarClipDistance() != list.at(0).toFloat()) + mCamera->setFarClipDistance(list.at(0).toFloat()); + } + + // minimise unnecessary ogre window creation by updating only when there is a change + if(key == "3d-render/antialiasing") + { + unsigned int aa = mWindow->getFSAA(); + unsigned int antialiasing = 0; + if(!list.empty()) + { + if(list.at(0) == "MSAA 16") antialiasing = 16; + else if(list.at(0) == "MSAA 8") antialiasing = 8; + else if(list.at(0) == "MSAA 4") antialiasing = 4; + else if(list.at(0) == "MSAA 2") antialiasing = 2; + } + if(aa != antialiasing) + updateOgreWindow(); + } + } } diff --git a/apps/opencs/view/render/scenewidget.hpp b/apps/opencs/view/render/scenewidget.hpp index 8f548f483..699d6a7a5 100644 --- a/apps/opencs/view/render/scenewidget.hpp +++ b/apps/opencs/view/render/scenewidget.hpp @@ -14,6 +14,9 @@ namespace Ogre class Camera; class SceneManager; class RenderWindow; + class Viewport; + class OverlaySystem; + class RenderTargetListener; } namespace CSVWidget @@ -42,11 +45,21 @@ namespace CSVRender ///< \attention The created tool is not added to the toolbar (via addTool). Doing that /// is the responsibility of the calling function. + virtual void setVisibilityMask (unsigned int mask); + + virtual void updateScene(); + protected: void setNavigation (Navigation *navigation); ///< \attention The ownership of \a navigation is not transferred to *this. + void addRenderTargetListener(Ogre::RenderTargetListener *listener); + + void removeRenderTargetListener(Ogre::RenderTargetListener *listener); + + Ogre::Viewport *getViewport(); + Ogre::SceneManager *getSceneManager(); Ogre::Camera *getCamera(); @@ -56,35 +69,37 @@ namespace CSVRender void setDefaultAmbient (const Ogre::ColourValue& colour); ///< \note The actual ambient colour may differ based on lighting settings. + virtual void updateOverlay(); + + virtual void mouseReleaseEvent (QMouseEvent *event); + + virtual void mouseMoveEvent (QMouseEvent *event); + + void wheelEvent (QWheelEvent *event); + + void keyPressEvent (QKeyEvent *event); + private: void paintEvent(QPaintEvent* e); void resizeEvent(QResizeEvent* e); bool event(QEvent* e); - void keyPressEvent (QKeyEvent *event); - void keyReleaseEvent (QKeyEvent *event); void focusOutEvent (QFocusEvent *event); - void wheelEvent (QWheelEvent *event); - void leaveEvent (QEvent *event); - void mouseMoveEvent (QMouseEvent *event); - - void mouseReleaseEvent (QMouseEvent *event); - void updateOgreWindow(); - int getFastFactor() const; - void setLighting (Lighting *lighting); ///< \attention The ownership of \a lighting is not transferred to *this. - Ogre::Camera* mCamera; + Ogre::Camera* mCamera; Ogre::SceneManager* mSceneMgr; Ogre::RenderWindow* mWindow; + Ogre::Viewport *mViewport; + Ogre::OverlaySystem *mOverlaySystem; Navigation *mNavigation; Lighting *mLighting; @@ -106,6 +121,10 @@ namespace CSVRender LightingNight mLightingNight; LightingBright mLightingBright; + public slots: + + void updateUserSetting (const QString &key, const QStringList &list); + private slots: void update(); diff --git a/apps/opencs/view/render/terrainstorage.cpp b/apps/opencs/view/render/terrainstorage.cpp new file mode 100644 index 000000000..a14eea5dd --- /dev/null +++ b/apps/opencs/view/render/terrainstorage.cpp @@ -0,0 +1,43 @@ +#include "terrainstorage.hpp" + +namespace CSVRender +{ + + TerrainStorage::TerrainStorage(const CSMWorld::Data &data) + : mData(data) + { + } + + ESM::Land* TerrainStorage::getLand(int cellX, int cellY) + { + std::ostringstream stream; + stream << "#" << cellX << " " << cellY; + + // The cell isn't guaranteed to have Land. This is because the terrain implementation + // has to wrap the vertices of the last row and column to the next cell, which may be a nonexisting cell + int index = mData.getLand().searchId(stream.str()); + if (index == -1) + return NULL; + + ESM::Land* land = mData.getLand().getRecord(index).get().mLand.get(); + int mask = ESM::Land::DATA_VHGT | ESM::Land::DATA_VNML | ESM::Land::DATA_VCLR | ESM::Land::DATA_VTEX; + if (!land->isDataLoaded(mask)) + land->loadData(mask); + return land; + } + + const ESM::LandTexture* TerrainStorage::getLandTexture(int index, short plugin) + { + std::ostringstream stream; + stream << index << "_" << plugin; + + return &mData.getLandTextures().getRecord(stream.str()).get(); + } + + void TerrainStorage::getBounds(float &minX, float &maxX, float &minY, float &maxY) + { + // not needed at the moment - this returns the bounds of the whole world, but we only edit individual cells + throw std::runtime_error("getBounds not implemented"); + } + +} diff --git a/apps/opencs/view/render/terrainstorage.hpp b/apps/opencs/view/render/terrainstorage.hpp new file mode 100644 index 000000000..97782ad17 --- /dev/null +++ b/apps/opencs/view/render/terrainstorage.hpp @@ -0,0 +1,29 @@ +#ifndef OPENCS_RENDER_TERRAINSTORAGE_H +#define OPENCS_RENDER_TERRAINSTORAGE_H + +#include + +#include "../../model/world/data.hpp" + +namespace CSVRender +{ + + /** + * @brief A bridge between the terrain component and OpenCS's terrain data storage. + */ + class TerrainStorage : public ESMTerrain::Storage + { + public: + TerrainStorage(const CSMWorld::Data& data); + private: + const CSMWorld::Data& mData; + + virtual ESM::Land* getLand (int cellX, int cellY); + virtual const ESM::LandTexture* getLandTexture(int index, short plugin); + + virtual void getBounds(float& minX, float& maxX, float& minY, float& maxY); + }; + +} + +#endif diff --git a/apps/opencs/view/render/textoverlay.cpp b/apps/opencs/view/render/textoverlay.cpp new file mode 100644 index 000000000..656ea959c --- /dev/null +++ b/apps/opencs/view/render/textoverlay.cpp @@ -0,0 +1,359 @@ +#include "textoverlay.hpp" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace CSVRender +{ + +// Things to do: +// - configurable font size in pixels (automatically calulate everything else from it) +// - configurable texture to use +// - try material script +// - decide whether to use QPaint (http://www.ogre3d.org/tikiwiki/Ogre+overlays+using+Qt) + +// http://www.ogre3d.org/tikiwiki/ObjectTextDisplay +// http://www.ogre3d.org/tikiwiki/MovableTextOverlay +// http://www.ogre3d.org/tikiwiki/Creating+dynamic+textures +// http://www.ogre3d.org/tikiwiki/ManualObject +TextOverlay::TextOverlay(const Ogre::MovableObject* obj, const Ogre::Camera* camera, const Ogre::String& id) + : mOverlay(0), mCaption(""), mDesc(""), mEnabled(true), mCamera(camera), mObj(obj), mId(id) + , mOnScreen(false) , mInstance(0), mFontHeight(16) // FIXME: make font height configurable +{ + if(id == "" || !camera || !obj) + throw std::runtime_error("TextOverlay could not be created."); + + // setup font + Ogre::FontManager &fontMgr = Ogre::FontManager::getSingleton(); + if (fontMgr.resourceExists("DejaVuLGC")) + mFont = fontMgr.getByName("DejaVuLGC","General"); + else + { + mFont = fontMgr.create("DejaVuLGC","General"); + mFont->setType(Ogre::FT_TRUETYPE); + mFont->setSource("DejaVuLGCSansMono.ttf"); + mFont->setTrueTypeSize(mFontHeight); + mFont->setTrueTypeResolution(96); + } + if(!mFont.isNull()) + mFont->load(); + else + throw std::runtime_error("TextOverlay font not loaded."); + + // setup overlay + Ogre::OverlayManager &overlayMgr = Ogre::OverlayManager::getSingleton(); + mOverlay = overlayMgr.getByName("CellIDPanel"+mId+Ogre::StringConverter::toString(mInstance)); + // FIXME: this logic is badly broken as it is possible to delete an earlier instance + while(mOverlay != NULL) + { + mInstance++; + mOverlay = overlayMgr.getByName("CellIDPanel"+mId+Ogre::StringConverter::toString(mInstance)); + } + mOverlay = overlayMgr.create("CellIDPanel"+mId+Ogre::StringConverter::toString(mInstance)); + + // create texture + Ogre::TexturePtr texture = Ogre::TextureManager::getSingleton().getByName("DynamicTransBlue"); + if(texture.isNull()) + { + texture = Ogre::TextureManager::getSingleton().createManual( + "DynamicTransBlue", // name + Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, + Ogre::TEX_TYPE_2D, // type + 8, 8, // width & height + 0, // number of mipmaps + Ogre::PF_BYTE_BGRA, // pixel format + Ogre::TU_DEFAULT); // usage; should be TU_DYNAMIC_WRITE_ONLY_DISCARDABLE for + // textures updated very often (e.g. each frame) + + Ogre::HardwarePixelBufferSharedPtr pixelBuffer = texture->getBuffer(); + pixelBuffer->lock(Ogre::HardwareBuffer::HBL_NORMAL); + const Ogre::PixelBox& pixelBox = pixelBuffer->getCurrentLock(); + + Ogre::uint8* pDest = static_cast(pixelBox.data); + + // Fill in some pixel data. This will give a semi-transparent blue, + // but this is of course dependent on the chosen pixel format. + for (size_t j = 0; j < 8; j++) + { + for(size_t i = 0; i < 8; i++) + { + *pDest++ = 255; // B + *pDest++ = 0; // G + *pDest++ = 0; // R + *pDest++ = 63; // A + } + + pDest += pixelBox.getRowSkip() * Ogre::PixelUtil::getNumElemBytes(pixelBox.format); + } + pixelBuffer->unlock(); + } + + // setup material for containers + Ogre::MaterialPtr mQuadMaterial = Ogre::MaterialManager::getSingleton().getByName( + "TransOverlayMaterial"); + if(mQuadMaterial.isNull()) + { + Ogre::MaterialPtr mQuadMaterial = Ogre::MaterialManager::getSingleton().create( + "TransOverlayMaterial", + Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, true ); + Ogre::Pass *pass = mQuadMaterial->getTechnique( 0 )->getPass( 0 ); + pass->setLightingEnabled( false ); + pass->setDepthWriteEnabled( false ); + pass->setSceneBlending( Ogre::SBT_TRANSPARENT_ALPHA ); + + Ogre::TextureUnitState *tex = pass->createTextureUnitState("MyCustomState", 0); + tex->setTextureName("DynamicTransBlue"); + tex->setTextureFiltering( Ogre::TFO_ANISOTROPIC ); + mQuadMaterial->load(); + } + + mContainer = static_cast(overlayMgr.createOverlayElement( + "Panel", "container"+mId +"#"+Ogre::StringConverter::toString(mInstance))); + mContainer->setMaterialName("TransOverlayMaterial"); + mOverlay->add2D(mContainer); + + // setup text area overlay element + mElement = static_cast(overlayMgr.createOverlayElement( + "TextArea", "text"+mId +"#"+Ogre::StringConverter::toString(mInstance))); + mElement->setMetricsMode(Ogre::GMM_RELATIVE); + mElement->setDimensions(1.0, 1.0); + mElement->setMetricsMode(Ogre::GMM_PIXELS); + mElement->setPosition(2*fontHeight()/3, 1.3*fontHeight()/3); // 1.3 & 2 = fudge factor + + mElement->setFontName("DejaVuLGC"); + mElement->setCharHeight(fontHeight()); // NOTE: seems that this is required as well as font->setTrueTypeSize() + mElement->setHorizontalAlignment(Ogre::GHA_LEFT); + //mElement->setColour(Ogre::ColourValue(1.0, 1.0, 1.0)); // R, G, B + mElement->setColour(Ogre::ColourValue(1.0, 1.0, 0)); // yellow + + mContainer->addChild(mElement); + mOverlay->show(); +} + +void TextOverlay::getScreenCoordinates(const Ogre::Vector3& position, Ogre::Real& x, Ogre::Real& y) +{ + Ogre::Vector3 hcsPosition = mCamera->getProjectionMatrix() * (mCamera->getViewMatrix() * position); + + x = 1.0f - ((hcsPosition.x * 0.5f) + 0.5f); // 0 <= x <= 1 // left := 0,right := 1 + y = ((hcsPosition.y * 0.5f) + 0.5f); // 0 <= y <= 1 // bottom := 0,top := 1 +} + +void TextOverlay::getMinMaxEdgesOfAABBIn2D(float& MinX, float& MinY, float& MaxX, float& MaxY, + bool top) +{ + MinX = 0, MinY = 0, MaxX = 0, MaxY = 0; + float X[4]; // the 2D dots of the AABB in screencoordinates + float Y[4]; + + if(!mObj->isInScene()) + return; + + const Ogre::AxisAlignedBox &AABB = mObj->getWorldBoundingBox(true); // the AABB of the target + Ogre::Vector3 cornersOfAABB[4]; + if(top) + { + cornersOfAABB[0] = AABB.getCorner(Ogre::AxisAlignedBox::FAR_LEFT_TOP); + cornersOfAABB[1] = AABB.getCorner(Ogre::AxisAlignedBox::FAR_RIGHT_TOP); + cornersOfAABB[2] = AABB.getCorner(Ogre::AxisAlignedBox::NEAR_LEFT_TOP); + cornersOfAABB[3] = AABB.getCorner(Ogre::AxisAlignedBox::NEAR_RIGHT_TOP); + } + else + { + cornersOfAABB[0] = AABB.getCorner(Ogre::AxisAlignedBox::FAR_LEFT_BOTTOM); + cornersOfAABB[1] = AABB.getCorner(Ogre::AxisAlignedBox::FAR_RIGHT_BOTTOM); + cornersOfAABB[2] = AABB.getCorner(Ogre::AxisAlignedBox::NEAR_LEFT_BOTTOM); + cornersOfAABB[3] = AABB.getCorner(Ogre::AxisAlignedBox::NEAR_RIGHT_BOTTOM); + } + + //The normal vector of the plane. This points directly infront of the camera. + Ogre::Vector3 cameraPlainNormal = mCamera->getDerivedOrientation().zAxis(); + + //the plane that devides the space before and behind the camera. + Ogre::Plane CameraPlane = Ogre::Plane(cameraPlainNormal, mCamera->getDerivedPosition()); + + for (int i = 0; i < 4; i++) + { + X[i] = 0; + Y[i] = 0; + + getScreenCoordinates(cornersOfAABB[i],X[i],Y[i]); // transfor into 2d dots + + if (CameraPlane.getSide(cornersOfAABB[i]) == Ogre::Plane::NEGATIVE_SIDE) + { + if (i == 0) // accept the first set of values, no matter how bad it might be. + { + MinX = X[i]; + MinY = Y[i]; + MaxX = X[i]; + MaxY = Y[i]; + } + else // now compare if you get "better" values + { + if (MinX > X[i]) MinX = X[i]; + if (MinY > Y[i]) MinY = Y[i]; + if (MaxX < X[i]) MaxX = X[i]; + if (MaxY < Y[i]) MaxY = Y[i]; + } + } + else + { + MinX = 0; + MinY = 0; + MaxX = 0; + MaxY = 0; + break; + } + } +} + +TextOverlay::~TextOverlay() +{ + Ogre::OverlayManager::OverlayMapIterator iter = Ogre::OverlayManager::getSingleton().getOverlayIterator(); + if(!iter.hasMoreElements()) + mOverlay->hide(); + + Ogre::OverlayManager *overlayMgr = Ogre::OverlayManager::getSingletonPtr(); + mContainer->removeChild("text"+mId+"#"+Ogre::StringConverter::toString(mInstance)); + mOverlay->remove2D(mContainer); + + if(!iter.hasMoreElements()) + overlayMgr->destroy(mOverlay); +} + +void TextOverlay::show(bool show) +{ + if(show && mOnScreen) + mContainer->show(); + else + mContainer->hide(); +} + +void TextOverlay::enable(bool enable) +{ + if(enable == mOverlay->isVisible()) + return; + + mEnabled = enable; + if(enable) + mOverlay->show(); + else + mOverlay->hide(); +} + +bool TextOverlay::isEnabled() +{ + return mEnabled; +} + +void TextOverlay::setCaption(const Ogre::String& text) +{ + if(mCaption == text) + return; + + mCaption = text; + mElement->setCaption(text); +} + +void TextOverlay::setDesc(const Ogre::String& text) +{ + if(mDesc == text) + return; + + mDesc = text; + mElement->setCaption(mCaption + ((text == "") ? "" : ("\n" + text))); +} + +Ogre::FontPtr TextOverlay::getFont() +{ + return mFont; +} + +int TextOverlay::textWidth() +{ + float captionWidth = 0; + float descWidth = 0; + + for(Ogre::String::const_iterator i = mCaption.begin(); i < mCaption.end(); ++i) + { + if(*i == 0x0020) + captionWidth += getFont()->getGlyphAspectRatio(0x0030); + else + captionWidth += getFont()->getGlyphAspectRatio(*i); + } + + for(Ogre::String::const_iterator i = mDesc.begin(); i < mDesc.end(); ++i) + { + if(*i == 0x0020) + descWidth += getFont()->getGlyphAspectRatio(0x0030); + else + descWidth += getFont()->getGlyphAspectRatio(*i); + } + + captionWidth *= fontHeight(); + descWidth *= fontHeight(); + + return (int) std::max(captionWidth, descWidth); +} + +int TextOverlay::fontHeight() +{ + return mFontHeight; +} + +void TextOverlay::update() +{ + float min_x, max_x, min_y, max_y; + getMinMaxEdgesOfAABBIn2D(min_x, min_y, max_x, max_y, false); + + if ((min_x>0.0) && (max_x<1.0) && (min_y>0.0) && (max_y<1.0)) + { + mOnScreen = true; + mContainer->show(); + } + else + { + mOnScreen = false; + mContainer->hide(); + return; + } + + getMinMaxEdgesOfAABBIn2D(min_x, min_y, max_x, max_y); + + Ogre::OverlayManager &overlayMgr = Ogre::OverlayManager::getSingleton(); + float viewportWidth = std::max(overlayMgr.getViewportWidth(), 1); // zero at the start + float viewportHeight = std::max(overlayMgr.getViewportHeight(), 1); // zero at the start + + int width = fontHeight()*2/3 + textWidth() + fontHeight()*2/3; // add margins + int height = fontHeight()/3 + fontHeight() + fontHeight()/3; + if(mDesc != "") + height = fontHeight()/3 + 2*fontHeight() + fontHeight()/3; + + float relTextWidth = width / viewportWidth; + float relTextHeight = height / viewportHeight; + + float posX = 1 - (min_x + max_x + relTextWidth)/2; + float posY = 1 - max_y - (relTextHeight-fontHeight()/3/viewportHeight); + + mContainer->setMetricsMode(Ogre::GMM_RELATIVE); + mContainer->setPosition(posX, posY); + mContainer->setDimensions(relTextWidth, relTextHeight); + + mPos = QRect(posX*viewportWidth, posY*viewportHeight, width, height); +} + +QRect TextOverlay::container() +{ + return mPos; +} + +} diff --git a/apps/opencs/view/render/textoverlay.hpp b/apps/opencs/view/render/textoverlay.hpp new file mode 100644 index 000000000..dbb347e56 --- /dev/null +++ b/apps/opencs/view/render/textoverlay.hpp @@ -0,0 +1,66 @@ +#ifndef OPENCS_VIEW_TEXTOVERLAY_H +#define OPENCS_VIEW_TEXTOVERLAY_H + +#include + +#include +#include + +namespace Ogre +{ + class MovableObject; + class Camera; + class Font; + class Overlay; + class OverlayContainer; + class TextAreaOverlayElement; +} + +namespace CSVRender +{ + + class TextOverlay + { + Ogre::Overlay* mOverlay; + Ogre::OverlayContainer* mContainer; + Ogre::TextAreaOverlayElement* mElement; + Ogre::String mCaption; + Ogre::String mDesc; + + const Ogre::MovableObject* mObj; + const Ogre::Camera* mCamera; + Ogre::FontPtr mFont; + int mFontHeight; // in pixels + Ogre::String mId; + QRect mPos; + + bool mEnabled; + bool mOnScreen; + int mInstance; + + Ogre::FontPtr getFont(); + int textWidth(); + int fontHeight(); + void getScreenCoordinates(const Ogre::Vector3& position, Ogre::Real& x, Ogre::Real& y); + void getMinMaxEdgesOfAABBIn2D(float& MinX, float& MinY, float& MaxX, float& MaxY, + bool top = true); + + public: + + TextOverlay(const Ogre::MovableObject* obj, const Ogre::Camera* camera, const Ogre::String &id); + virtual ~TextOverlay(); + + void enable(bool enable); // controlled from scene widget toolbar visibility mask + void show(bool show); // for updating from render target listener + bool isEnabled(); + void setCaption(const Ogre::String& text); + void setDesc(const Ogre::String& text); + void update(); + QRect container(); // for detection of mouse click on the overlay + Ogre::String getCaption() { return mCaption; } // FIXME: debug + Ogre::String getDesc() { return mDesc; } + }; + +} + +#endif // OPENCS_VIEW_TEXTOVERLAY_H diff --git a/apps/opencs/view/render/unpagedworldspacewidget.cpp b/apps/opencs/view/render/unpagedworldspacewidget.cpp index 0b656ddc6..462b62b7a 100644 --- a/apps/opencs/view/render/unpagedworldspacewidget.cpp +++ b/apps/opencs/view/render/unpagedworldspacewidget.cpp @@ -1,7 +1,10 @@ #include "unpagedworldspacewidget.hpp" +#include + #include +#include #include @@ -11,6 +14,11 @@ #include "../../model/world/idtable.hpp" #include "../../model/world/tablemimedata.hpp" +#include "../widget/scenetooltoggle.hpp" +#include "../widget/scenetooltoggle2.hpp" + +#include "elements.hpp" + void CSVRender::UnpagedWorldspaceWidget::update() { const CSMWorld::Record& record = @@ -41,7 +49,7 @@ CSVRender::UnpagedWorldspaceWidget::UnpagedWorldspaceWidget (const std::string& update(); - mCell.reset (new Cell (document.getData(), getSceneManager(), mCellId)); + mCell.reset (new Cell (document.getData(), getSceneManager(), mCellId, document.getPhysics())); } void CSVRender::UnpagedWorldspaceWidget::cellDataChanged (const QModelIndex& topLeft, @@ -74,13 +82,21 @@ void CSVRender::UnpagedWorldspaceWidget::cellRowsAboutToBeRemoved (const QModelI emit closeRequest(); } -void CSVRender::UnpagedWorldspaceWidget::handleDrop (const std::vector< CSMWorld::UniversalId >& data) +bool CSVRender::UnpagedWorldspaceWidget::handleDrop (const std::vector& data, DropType type) { + if (WorldspaceWidget::handleDrop (data, type)) + return true; + + if (type!=Type_CellsInterior) + return false; + mCellId = data.begin()->getId(); + mCell.reset (new Cell (getDocument().getData(), getSceneManager(), mCellId, getDocument().getPhysics())); + update(); emit cellChanged(*data.begin()); - /// \todo replace mCell + return true; } void CSVRender::UnpagedWorldspaceWidget::referenceableDataChanged (const QModelIndex& topLeft, @@ -137,14 +153,41 @@ void CSVRender::UnpagedWorldspaceWidget::referenceAdded (const QModelIndex& pare flagAsModified(); } -CSVRender::WorldspaceWidget::dropRequirments CSVRender::UnpagedWorldspaceWidget::getDropRequirements (CSVRender::WorldspaceWidget::dropType type) const +void CSVRender::UnpagedWorldspaceWidget::addVisibilitySelectorButtons ( + CSVWidget::SceneToolToggle2 *tool) +{ + WorldspaceWidget::addVisibilitySelectorButtons (tool); + tool->addButton (Element_Terrain, "Terrain", "", true); + tool->addButton (Element_Fog, "Fog"); +} + +std::string CSVRender::UnpagedWorldspaceWidget::getStartupInstruction() { + Ogre::Vector3 position = getCamera()->getPosition(); + + std::ostringstream stream; + + stream + << "player->positionCell " + << position.x << ", " << position.y << ", " << position.z + << ", 0, \"" << mCellId << "\""; + + return stream.str(); +} + +CSVRender::WorldspaceWidget::dropRequirments CSVRender::UnpagedWorldspaceWidget::getDropRequirements (CSVRender::WorldspaceWidget::DropType type) const +{ + dropRequirments requirements = WorldspaceWidget::getDropRequirements (type); + + if (requirements!=ignored) + return requirements; + switch(type) { - case cellsInterior: + case Type_CellsInterior: return canHandle; - case cellsExterior: + case Type_CellsExterior: return needPaged; default: diff --git a/apps/opencs/view/render/unpagedworldspacewidget.hpp b/apps/opencs/view/render/unpagedworldspacewidget.hpp index ee8377fae..d01c3e766 100644 --- a/apps/opencs/view/render/unpagedworldspacewidget.hpp +++ b/apps/opencs/view/render/unpagedworldspacewidget.hpp @@ -37,9 +37,11 @@ namespace CSVRender UnpagedWorldspaceWidget (const std::string& cellId, CSMDoc::Document& document, QWidget *parent); - virtual dropRequirments getDropRequirements(dropType type) const; + virtual dropRequirments getDropRequirements(DropType type) const; - virtual void handleDrop(const std::vector& data); + /// \return Drop handled? + virtual bool handleDrop (const std::vector& data, + DropType type); private: @@ -56,6 +58,12 @@ namespace CSVRender virtual void referenceAdded (const QModelIndex& index, int start, int end); + virtual std::string getStartupInstruction(); + + protected: + + virtual void addVisibilitySelectorButtons (CSVWidget::SceneToolToggle2 *tool); + private slots: void cellDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); diff --git a/apps/opencs/view/render/worldspacewidget.cpp b/apps/opencs/view/render/worldspacewidget.cpp index d3413a29d..582ccea64 100644 --- a/apps/opencs/view/render/worldspacewidget.cpp +++ b/apps/opencs/view/render/worldspacewidget.cpp @@ -1,6 +1,8 @@ #include "worldspacewidget.hpp" +#include + #include #include #include @@ -8,11 +10,20 @@ #include #include "../../model/world/universalid.hpp" +#include "../../model/world/idtable.hpp" #include "../widget/scenetoolmode.hpp" +#include "../widget/scenetooltoggle2.hpp" +#include "../widget/scenetoolrun.hpp" + +#include "../world/physicssystem.hpp" + +#include "elements.hpp" +#include "editmode.hpp" CSVRender::WorldspaceWidget::WorldspaceWidget (CSMDoc::Document& document, QWidget* parent) -: SceneWidget (parent), mDocument(document) +: SceneWidget (parent), mDocument(document), mSceneElements(0), mRun(0), mPhysics(boost::shared_ptr()), mMouse(0), + mInteractionMask (0) { setAcceptDrops(true); @@ -35,6 +46,24 @@ CSVRender::WorldspaceWidget::WorldspaceWidget (CSMDoc::Document& document, QWidg this, SLOT (referenceAboutToBeRemoved (const QModelIndex&, int, int))); connect (references, SIGNAL (rowsInserted (const QModelIndex&, int, int)), this, SLOT (referenceAdded (const QModelIndex&, int, int))); + + QAbstractItemModel *debugProfiles = + document.getData().getTableModel (CSMWorld::UniversalId::Type_DebugProfiles); + + connect (debugProfiles, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), + this, SLOT (debugProfileDataChanged (const QModelIndex&, const QModelIndex&))); + connect (debugProfiles, SIGNAL (rowsAboutToBeRemoved (const QModelIndex&, int, int)), + this, SLOT (debugProfileAboutToBeRemoved (const QModelIndex&, int, int))); + + mPhysics = document.getPhysics(); // create physics if one doesn't exist + mPhysics->addSceneManager(getSceneManager(), this); + mMouse = new MouseState(this); +} + +CSVRender::WorldspaceWidget::~WorldspaceWidget () +{ + delete mMouse; + mPhysics->removeSceneManager(getSceneManager()); } void CSVRender::WorldspaceWidget::selectNavigationMode (const std::string& mode) @@ -61,7 +90,7 @@ CSVWidget::SceneToolMode *CSVRender::WorldspaceWidget::makeNavigationSelector ( /// \todo replace icons /// \todo consider user-defined button-mapping - tool->addButton (":door.png", "1st", + tool->addButton (":scenetoolbar/1st-person", "1st", "First Person" "
  • Mouse-Look while holding the left button
  • " "
  • WASD movement keys
  • " @@ -70,7 +99,7 @@ CSVWidget::SceneToolMode *CSVRender::WorldspaceWidget::makeNavigationSelector ( "
  • Camera is held upright
  • " "
  • Hold shift to speed up movement
  • " "
"); - tool->addButton (":GMST.png", "free", + tool->addButton (":scenetoolbar/free-camera", "free", "Free Camera" "
  • Mouse-Look while holding the left button
  • " "
  • Stafing (also vertically) via WASD or by holding the left mouse button and control
  • " @@ -78,7 +107,7 @@ CSVWidget::SceneToolMode *CSVRender::WorldspaceWidget::makeNavigationSelector ( "
  • Roll camera with Q and E keys
  • " "
  • Hold shift to speed up movement
  • " "
"); - tool->addButton (":Info.png", "orbit", + tool->addButton (":scenetoolbar/orbiting-camera", "orbit", "Orbiting Camera" "
  • Always facing the centre point
  • " "
  • Rotate around the centre point via WASD or by moving the mouse while holding the left button
  • " @@ -94,59 +123,162 @@ CSVWidget::SceneToolMode *CSVRender::WorldspaceWidget::makeNavigationSelector ( return tool; } -CSVRender::WorldspaceWidget::dropType CSVRender::WorldspaceWidget::getDropType ( +CSVWidget::SceneToolToggle2 *CSVRender::WorldspaceWidget::makeSceneVisibilitySelector (CSVWidget::SceneToolbar *parent) +{ + mSceneElements = new CSVWidget::SceneToolToggle2 (parent, + "Scene Element Visibility", ":scenetoolbar/scene-view-c", ":scenetoolbar/scene-view-"); + + addVisibilitySelectorButtons (mSceneElements); + + mSceneElements->setSelection (0xffffffff); + + connect (mSceneElements, SIGNAL (selectionChanged()), + this, SLOT (elementSelectionChanged())); + + return mSceneElements; +} + +CSVWidget::SceneToolRun *CSVRender::WorldspaceWidget::makeRunTool ( + CSVWidget::SceneToolbar *parent) +{ + CSMWorld::IdTable& debugProfiles = dynamic_cast ( + *mDocument.getData().getTableModel (CSMWorld::UniversalId::Type_DebugProfiles)); + + std::vector profiles; + + int idColumn = debugProfiles.findColumnIndex (CSMWorld::Columns::ColumnId_Id); + int stateColumn = debugProfiles.findColumnIndex (CSMWorld::Columns::ColumnId_Modification); + int defaultColumn = debugProfiles.findColumnIndex ( + CSMWorld::Columns::ColumnId_DefaultProfile); + + int size = debugProfiles.rowCount(); + + for (int i=0; i& data) { - dropType output = notCells; - bool firstIteration = true; + DropType output = Type_Other; - for (unsigned i = 0; i < data.size(); ++i) + for (std::vector::const_iterator iter (data.begin()); + iter!=data.end(); ++iter) { - if (data[i].getType() == CSMWorld::UniversalId::Type_Cell || - data[i].getType() == CSMWorld::UniversalId::Type_Cell_Missing) + DropType type = Type_Other; + + if (iter->getType()==CSMWorld::UniversalId::Type_Cell || + iter->getType()==CSMWorld::UniversalId::Type_Cell_Missing) { - if (*(data[i].getId().begin()) == '#') //exterior - { - if (firstIteration) - { - output = cellsExterior; - firstIteration = false; - continue; - } - - if (output == cellsInterior) - { - output = cellsMixed; - break; - } else { - output = cellsInterior; - } - } else //interior - { - if (firstIteration) - { - output = cellsInterior; - firstIteration = false; - continue; - } - - if (output == cellsExterior) - { - output = cellsMixed; - break; - } else { - output = cellsInterior; - } - } - } else { - output = notCells; - break; + type = iter->getId().substr (0, 1)=="#" ? Type_CellsExterior : Type_CellsInterior; } + else if (iter->getType()==CSMWorld::UniversalId::Type_DebugProfile) + type = Type_DebugProfile; + + if (iter==data.begin()) + output = type; + else if (output!=type) // mixed types -> ignore + return Type_Other; } return output; } +CSVRender::WorldspaceWidget::dropRequirments + CSVRender::WorldspaceWidget::getDropRequirements (DropType type) const +{ + if (type==Type_DebugProfile) + return canHandle; + + return ignored; +} + +bool CSVRender::WorldspaceWidget::handleDrop (const std::vector& data, + DropType type) +{ + if (type==Type_DebugProfile) + { + if (mRun) + { + for (std::vector::const_iterator iter (data.begin()); + iter!=data.end(); ++iter) + mRun->addProfile (iter->getId()); + } + + return true; + } + + return false; +} + +unsigned int CSVRender::WorldspaceWidget::getVisibilityMask() const +{ + return mSceneElements->getSelection(); +} + +void CSVRender::WorldspaceWidget::setInteractionMask (unsigned int mask) +{ + mInteractionMask = mask | Element_CellMarker | Element_CellArrow; +} + +unsigned int CSVRender::WorldspaceWidget::getInteractionMask() const +{ + return mInteractionMask & getVisibilityMask(); +} + +void CSVRender::WorldspaceWidget::addVisibilitySelectorButtons ( + CSVWidget::SceneToolToggle2 *tool) +{ + tool->addButton (Element_Reference, "References"); + tool->addButton (Element_Water, "Water"); + tool->addButton (Element_Pathgrid, "Pathgrid"); +} + +void CSVRender::WorldspaceWidget::addEditModeSelectorButtons (CSVWidget::SceneToolMode *tool) +{ + /// \todo replace EditMode with suitable subclasses + tool->addButton ( + new EditMode (this, QIcon (":placeholder"), Element_Reference, "Reference editing"), + "object"); + tool->addButton ( + new EditMode (this, QIcon (":placeholder"), Element_Pathgrid, "Pathgrid editing"), + "pathgrid"); +} + +CSMDoc::Document& CSVRender::WorldspaceWidget::getDocument() +{ + return mDocument; +} + void CSVRender::WorldspaceWidget::dragEnterEvent (QDragEnterEvent* event) { event->accept(); @@ -157,7 +289,6 @@ void CSVRender::WorldspaceWidget::dragMoveEvent(QDragMoveEvent *event) event->accept(); } - void CSVRender::WorldspaceWidget::dropEvent (QDropEvent* event) { const CSMWorld::TableMimeData* mime = dynamic_cast (event->mimeData()); @@ -169,3 +300,123 @@ void CSVRender::WorldspaceWidget::dropEvent (QDropEvent* event) emit dataDropped(mime->getData()); } //not handling drops from different documents at the moment } + +void CSVRender::WorldspaceWidget::runRequest (const std::string& profile) +{ + mDocument.startRunning (profile, getStartupInstruction()); +} + +void CSVRender::WorldspaceWidget::debugProfileDataChanged (const QModelIndex& topLeft, + const QModelIndex& bottomRight) +{ + if (!mRun) + return; + + CSMWorld::IdTable& debugProfiles = dynamic_cast ( + *mDocument.getData().getTableModel (CSMWorld::UniversalId::Type_DebugProfiles)); + + int idColumn = debugProfiles.findColumnIndex (CSMWorld::Columns::ColumnId_Id); + int stateColumn = debugProfiles.findColumnIndex (CSMWorld::Columns::ColumnId_Modification); + + for (int i=topLeft.row(); i<=bottomRight.row(); ++i) + { + int state = debugProfiles.data (debugProfiles.index (i, stateColumn)).toInt(); + + // As of version 0.33 this case can not happen because debug profiles exist only in + // project or session scope, which means they will never be in deleted state. But we + // are adding the code for the sake of completeness and to avoid surprises if debug + // profile ever get extended to content scope. + if (state==CSMWorld::RecordBase::State_Deleted) + mRun->removeProfile (debugProfiles.data ( + debugProfiles.index (i, idColumn)).toString().toUtf8().constData()); + } +} + +void CSVRender::WorldspaceWidget::debugProfileAboutToBeRemoved (const QModelIndex& parent, + int start, int end) +{ + if (parent.isValid()) + return; + + if (!mRun) + return; + + CSMWorld::IdTable& debugProfiles = dynamic_cast ( + *mDocument.getData().getTableModel (CSMWorld::UniversalId::Type_DebugProfiles)); + + int idColumn = debugProfiles.findColumnIndex (CSMWorld::Columns::ColumnId_Id); + + for (int i=start; i<=end; ++i) + { + mRun->removeProfile (debugProfiles.data ( + debugProfiles.index (i, idColumn)).toString().toUtf8().constData()); + } +} + +void CSVRender::WorldspaceWidget::elementSelectionChanged() +{ + setVisibilityMask (getVisibilityMask()); + flagAsModified(); + updateOverlay(); +} + +void CSVRender::WorldspaceWidget::updateOverlay() +{ +} + +void CSVRender::WorldspaceWidget::mouseMoveEvent (QMouseEvent *event) +{ + if(event->buttons() & Qt::RightButton) + { + mMouse->mouseMoveEvent(event); + } + SceneWidget::mouseMoveEvent(event); +} + +void CSVRender::WorldspaceWidget::mousePressEvent (QMouseEvent *event) +{ + if(event->buttons() & Qt::RightButton) + { + mMouse->mousePressEvent(event); + } + //SceneWidget::mousePressEvent(event); +} + +void CSVRender::WorldspaceWidget::mouseReleaseEvent (QMouseEvent *event) +{ + if(event->button() == Qt::RightButton) + { + if(!getViewport()) + { + SceneWidget::mouseReleaseEvent(event); + return; + } + mMouse->mouseReleaseEvent(event); + } + SceneWidget::mouseReleaseEvent(event); +} + +void CSVRender::WorldspaceWidget::mouseDoubleClickEvent (QMouseEvent *event) +{ + if(event->button() == Qt::RightButton) + { + mMouse->mouseDoubleClickEvent(event); + } + //SceneWidget::mouseDoubleClickEvent(event); +} + +void CSVRender::WorldspaceWidget::wheelEvent (QWheelEvent *event) +{ + if(!mMouse->wheelEvent(event)) + SceneWidget::wheelEvent(event); +} + +void CSVRender::WorldspaceWidget::keyPressEvent (QKeyEvent *event) +{ + if(event->key() == Qt::Key_Escape) + { + mMouse->cancelDrag(); + } + else + SceneWidget::keyPressEvent(event); +} diff --git a/apps/opencs/view/render/worldspacewidget.hpp b/apps/opencs/view/render/worldspacewidget.hpp index 3b96779a8..b19197e36 100644 --- a/apps/opencs/view/render/worldspacewidget.hpp +++ b/apps/opencs/view/render/worldspacewidget.hpp @@ -1,7 +1,10 @@ #ifndef OPENCS_VIEW_WORLDSPACEWIDGET_H #define OPENCS_VIEW_WORLDSPACEWIDGET_H +#include + #include "scenewidget.hpp" +#include "mousestate.hpp" #include "navigation1st.hpp" #include "navigationfree.hpp" @@ -13,10 +16,18 @@ namespace CSMWorld { class UniversalId; } + namespace CSVWidget { class SceneToolMode; + class SceneToolToggle2; class SceneToolbar; + class SceneToolRun; +} + +namespace CSVWorld +{ + class PhysicsSystem; } namespace CSVRender @@ -28,15 +39,21 @@ namespace CSVRender CSVRender::Navigation1st m1st; CSVRender::NavigationFree mFree; CSVRender::NavigationOrbit mOrbit; + CSVWidget::SceneToolToggle2 *mSceneElements; + CSVWidget::SceneToolRun *mRun; + CSMDoc::Document& mDocument; + boost::shared_ptr mPhysics; + MouseState *mMouse; + unsigned int mInteractionMask; public: - enum dropType + enum DropType { - cellsMixed, - cellsInterior, - cellsExterior, - notCells + Type_CellsInterior, + Type_CellsExterior, + Type_Other, + Type_DebugProfile }; enum dropRequirments @@ -48,24 +65,64 @@ namespace CSVRender }; WorldspaceWidget (CSMDoc::Document& document, QWidget *parent = 0); + ~WorldspaceWidget (); CSVWidget::SceneToolMode *makeNavigationSelector (CSVWidget::SceneToolbar *parent); ///< \attention The created tool is not added to the toolbar (via addTool). Doing that /// is the responsibility of the calling function. + /// \attention The created tool is not added to the toolbar (via addTool). Doing + /// that is the responsibility of the calling function. + CSVWidget::SceneToolToggle2 *makeSceneVisibilitySelector ( + CSVWidget::SceneToolbar *parent); + + /// \attention The created tool is not added to the toolbar (via addTool). Doing + /// that is the responsibility of the calling function. + CSVWidget::SceneToolRun *makeRunTool (CSVWidget::SceneToolbar *parent); + + /// \attention The created tool is not added to the toolbar (via addTool). Doing + /// that is the responsibility of the calling function. + CSVWidget::SceneToolMode *makeEditModeSelector (CSVWidget::SceneToolbar *parent); + void selectDefaultNavigationMode(); - static dropType getDropType(const std::vector& data); + static DropType getDropType(const std::vector& data); - virtual dropRequirments getDropRequirements(dropType type) const = 0; + virtual dropRequirments getDropRequirements(DropType type) const; virtual void useViewHint (const std::string& hint); ///< Default-implementation: ignored. - virtual void handleDrop(const std::vector& data) = 0; + /// \return Drop handled? + virtual bool handleDrop (const std::vector& data, + DropType type); + + virtual unsigned int getVisibilityMask() const; + + /// \note This function will implicitly add elements that are independent of the + /// selected edit mode. + virtual void setInteractionMask (unsigned int mask); + + /// \note This function will only return those elements that are both visible and + /// marked for interaction. + unsigned int getInteractionMask() const; protected: - const CSMDoc::Document& mDocument; //for checking if drop comes from same document + + virtual void addVisibilitySelectorButtons (CSVWidget::SceneToolToggle2 *tool); + + virtual void addEditModeSelectorButtons (CSVWidget::SceneToolMode *tool); + + CSMDoc::Document& getDocument(); + + virtual void updateOverlay(); + + virtual void mouseMoveEvent (QMouseEvent *event); + virtual void mousePressEvent (QMouseEvent *event); + virtual void mouseReleaseEvent (QMouseEvent *event); + virtual void mouseDoubleClickEvent (QMouseEvent *event); + virtual void wheelEvent (QWheelEvent *event); + virtual void keyPressEvent (QKeyEvent *event); private: @@ -75,6 +132,8 @@ namespace CSVRender void dragMoveEvent(QDragMoveEvent *event); + virtual std::string getStartupInstruction() = 0; + private slots: void selectNavigationMode (const std::string& mode); @@ -92,11 +151,26 @@ namespace CSVRender virtual void referenceAdded (const QModelIndex& index, int start, int end) = 0; + virtual void runRequest (const std::string& profile); + + void debugProfileDataChanged (const QModelIndex& topLeft, + const QModelIndex& bottomRight); + + void debugProfileAboutToBeRemoved (const QModelIndex& parent, int start, int end); + + + protected slots: + + void elementSelectionChanged(); + signals: void closeRequest(); + void dataDropped(const std::vector& data); + + friend class MouseState; }; } -#endif \ No newline at end of file +#endif diff --git a/apps/opencs/view/settings/booleanview.cpp b/apps/opencs/view/settings/booleanview.cpp index 2a3f0cba6..29f9775af 100644 --- a/apps/opencs/view/settings/booleanview.cpp +++ b/apps/opencs/view/settings/booleanview.cpp @@ -12,16 +12,27 @@ CSVSettings::BooleanView::BooleanView (CSMSettings::Setting *setting, Page *parent) - : View (setting, parent) + : mType(setting->type()), View (setting, parent) { foreach (const QString &value, setting->declaredValues()) { QAbstractButton *button = 0; - switch (setting->type()) + switch (mType) { - case CSMSettings::Type_CheckBox: - button = new QCheckBox (value, this); + case CSMSettings::Type_CheckBox: { + if(mButtons.empty()) // show only one for checkboxes + { + button = new QCheckBox (value, this); + button->setChecked (setting->defaultValues().at(0) == "true" ? true : false); + + // special visual treatment option for checkboxes + if(setting->specialValueText() != "") { + Frame::setTitle(""); + button->setText(setting->specialValueText()); + } + } + } break; case CSMSettings::Type_RadioButton: @@ -32,14 +43,17 @@ CSVSettings::BooleanView::BooleanView (CSMSettings::Setting *setting, break; } - connect (button, SIGNAL (clicked (bool)), - this, SLOT (slotToggled (bool))); + if(button && (mType != CSMSettings::Type_CheckBox || mButtons.empty())) + { + connect (button, SIGNAL (clicked (bool)), + this, SLOT (slotToggled (bool))); - button->setObjectName (value); + button->setObjectName (value); - addWidget (button); + addWidget (button); - mButtons[value] = button; + mButtons[value] = button; + } } } @@ -53,8 +67,14 @@ void CSVSettings::BooleanView::slotToggled (bool state) foreach (QString key, mButtons.keys()) { - if (mButtons.value(key)->isChecked()) - values.append (key); + // checkbox values are true/false unlike radio buttons + if(mType == CSMSettings::Type_CheckBox) + values.append(mButtons.value(key)->isChecked() ? "true" : "false"); + else + { + if (mButtons.value(key)->isChecked()) + values.append (key); + } } setSelectedValues (values, false); diff --git a/apps/opencs/view/settings/booleanview.hpp b/apps/opencs/view/settings/booleanview.hpp index 55ef0bb08..53198234a 100644 --- a/apps/opencs/view/settings/booleanview.hpp +++ b/apps/opencs/view/settings/booleanview.hpp @@ -16,6 +16,7 @@ namespace CSVSettings Q_OBJECT QMap mButtons; + enum CSMSettings::SettingType mType; public: explicit BooleanView (CSMSettings::Setting *setting, diff --git a/apps/opencs/view/settings/dialog.cpp b/apps/opencs/view/settings/dialog.cpp index 56bc1fdfe..0b1231266 100644 --- a/apps/opencs/view/settings/dialog.cpp +++ b/apps/opencs/view/settings/dialog.cpp @@ -1,10 +1,13 @@ #include "dialog.hpp" +#include + #include #include #include #include #include +#include #include "../../model/settings/usersettings.hpp" @@ -12,8 +15,6 @@ #include -#include - #include #include #include @@ -26,6 +27,10 @@ CSVSettings::Dialog::Dialog(QMainWindow *parent) { setWindowTitle(QString::fromUtf8 ("User Settings")); + setSizePolicy (QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); + + setMinimumSize (600, 400); + setupDialog(); connect (mPageListWidget, @@ -39,20 +44,14 @@ void CSVSettings::Dialog::slotChangePage { mStackedWidget->changePage (mPageListWidget->row (cur), mPageListWidget->row (prev)); - - layout()->activate(); - setFixedSize(minimumSizeHint()); } void CSVSettings::Dialog::setupDialog() { - //create central widget with it's layout and immediate children - QWidget *centralWidget = new QGroupBox (this); + QSplitter *centralWidget = new QSplitter (this); + centralWidget->setSizePolicy (QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); - centralWidget->setLayout (new QHBoxLayout()); - centralWidget->setSizePolicy (QSizePolicy::Expanding, QSizePolicy::Preferred); setCentralWidget (centralWidget); - setDockOptions (QMainWindow::AllowNestedDocks); buildPageListWidget (centralWidget); buildStackedWidget (centralWidget); @@ -64,37 +63,39 @@ void CSVSettings::Dialog::buildPages() QFontMetrics fm (QApplication::font()); + int maxWidth = 1; + foreach (Page *page, SettingWindow::pages()) { - QString pageName = page->objectName(); + maxWidth = std::max (maxWidth, fm.width(page->getLabel())); - int textWidth = fm.width(pageName); + new QListWidgetItem (page->getLabel(), mPageListWidget); - new QListWidgetItem (pageName, mPageListWidget); - mPageListWidget->setFixedWidth (textWidth + 50); - - mStackedWidget->addWidget (&dynamic_cast(*(page))); + mStackedWidget->addWidget (page); } + mPageListWidget->setMaximumWidth (maxWidth + 10); + resize (mStackedWidget->sizeHint()); } -void CSVSettings::Dialog::buildPageListWidget (QWidget *centralWidget) +void CSVSettings::Dialog::buildPageListWidget (QSplitter *centralWidget) { mPageListWidget = new QListWidget (centralWidget); mPageListWidget->setMinimumWidth(50); - mPageListWidget->setSizePolicy (QSizePolicy::Fixed, QSizePolicy::Expanding); + mPageListWidget->setSizePolicy (QSizePolicy::Expanding, QSizePolicy::Expanding); mPageListWidget->setSelectionBehavior (QAbstractItemView::SelectItems); - centralWidget->layout()->addWidget(mPageListWidget); + centralWidget->addWidget(mPageListWidget); } -void CSVSettings::Dialog::buildStackedWidget (QWidget *centralWidget) +void CSVSettings::Dialog::buildStackedWidget (QSplitter *centralWidget) { mStackedWidget = new ResizeableStackedWidget (centralWidget); + mStackedWidget->setSizePolicy (QSizePolicy::Preferred, QSizePolicy::Expanding); - centralWidget->layout()->addWidget (mStackedWidget); + centralWidget->addWidget (mStackedWidget); } void CSVSettings::Dialog::closeEvent (QCloseEvent *event) @@ -114,8 +115,20 @@ void CSVSettings::Dialog::show() setViewValues(); } - QPoint screenCenter = QApplication::desktop()->screenGeometry().center(); - - move (screenCenter - geometry().center()); + QWidget *currView = QApplication::activeWindow(); + if(currView) + { + // place at the center of the window with focus + QSize size = currView->size(); + move(currView->geometry().x()+(size.width() - frameGeometry().width())/2, + currView->geometry().y()+(size.height() - frameGeometry().height())/2); + } + else + { + // something's gone wrong, place at the center of the screen + QPoint screenCenter = QApplication::desktop()->screenGeometry().center(); + move(screenCenter - QPoint(frameGeometry().width()/2, + frameGeometry().height()/2)); + } QWidget::show(); } diff --git a/apps/opencs/view/settings/dialog.hpp b/apps/opencs/view/settings/dialog.hpp index b0e12c461..cb85bddb9 100644 --- a/apps/opencs/view/settings/dialog.hpp +++ b/apps/opencs/view/settings/dialog.hpp @@ -8,6 +8,7 @@ class QStackedWidget; class QListWidget; class QListWidgetItem; +class QSplitter; namespace CSVSettings { @@ -39,8 +40,8 @@ namespace CSVSettings { private: void buildPages(); - void buildPageListWidget (QWidget *centralWidget); - void buildStackedWidget (QWidget *centralWidget); + void buildPageListWidget (QSplitter *centralWidget); + void buildStackedWidget (QSplitter *centralWidget); public slots: diff --git a/apps/opencs/view/settings/frame.cpp b/apps/opencs/view/settings/frame.cpp index 019024776..32e094274 100644 --- a/apps/opencs/view/settings/frame.cpp +++ b/apps/opencs/view/settings/frame.cpp @@ -3,7 +3,7 @@ #include const QString CSVSettings::Frame::sInvisibleBoxStyle = - QString::fromUtf8("Frame { border:2px; padding 2px; margin: 2px;}"); + QString::fromUtf8("Frame { border:2px; padding: 2px; margin: 2px;}"); CSVSettings::Frame::Frame (bool isVisible, const QString &title, QWidget *parent) @@ -14,7 +14,10 @@ CSVSettings::Frame::Frame (bool isVisible, const QString &title, mVisibleBoxStyle = styleSheet(); if (!isVisible) + { + // must be Page, not a View setStyleSheet (sInvisibleBoxStyle); + } setLayout (mLayout); } @@ -35,7 +38,7 @@ void CSVSettings::Frame::hideWidgets() QWidget *widg = static_cast (obj); if (widg->property("sizePolicy").isValid()) - widg->setSizePolicy (QSizePolicy::Ignored, QSizePolicy::Ignored); + widg->setSizePolicy (QSizePolicy::Expanding, QSizePolicy::Expanding); } layout()->activate(); diff --git a/apps/opencs/view/settings/page.cpp b/apps/opencs/view/settings/page.cpp index afd4bff5e..e846840b8 100644 --- a/apps/opencs/view/settings/page.cpp +++ b/apps/opencs/view/settings/page.cpp @@ -1,4 +1,7 @@ #include "page.hpp" + +#include + #include "view.hpp" #include "booleanview.hpp" #include "textview.hpp" @@ -14,10 +17,9 @@ QMap CSVSettings::Page::mViewFactories; -CSVSettings::Page::Page(const QString &pageName, - QList settingList, - SettingWindow *parent) : - mParent(parent), mIsEditorPage (false), Frame(false, "", parent) +CSVSettings::Page::Page (const QString &pageName, QList settingList, + SettingWindow *parent, const QString& label) +: mParent(parent), mIsEditorPage (false), Frame(false, "", parent), mLabel (label) { setObjectName (pageName); @@ -38,7 +40,18 @@ void CSVSettings::Page::setupViews void CSVSettings::Page::addView (CSMSettings::Setting *setting) { if (setting->viewType() == ViewType_Undefined) - return; + { + if(setting->specialValueText() != "") + { + // hack to put a label + addWidget(new QLabel(setting->specialValueText()), + setting->viewRow(), setting->viewColumn(), + setting->rowSpan(), setting->columnSpan()); + return; + } + else + return; + } View *view = mViewFactories[setting->viewType()]->createView(setting, this); @@ -90,3 +103,8 @@ void CSVSettings::Page::buildFactories() mViewFactories[ViewType_List] = new ListViewFactory (this); mViewFactories[ViewType_Range] = new RangeViewFactory (this); } + +QString CSVSettings::Page::getLabel() const +{ + return mLabel; +} diff --git a/apps/opencs/view/settings/page.hpp b/apps/opencs/view/settings/page.hpp index 877d4bef8..caf2eec3f 100644 --- a/apps/opencs/view/settings/page.hpp +++ b/apps/opencs/view/settings/page.hpp @@ -25,11 +25,11 @@ namespace CSVSettings SettingWindow *mParent; static QMap mViewFactories; bool mIsEditorPage; + QString mLabel; public: - explicit Page(const QString &pageName, - QList settingList, - SettingWindow *parent); + Page (const QString &pageName, QList settingList, + SettingWindow *parent, const QString& label); ///Creates a new view based on the passed setting and adds it to ///the page. @@ -42,6 +42,8 @@ namespace CSVSettings ///returns the list of views associated with the page const QList &views () const { return mViews; } + QString getLabel() const; + private: ///Creates views based on the passed setting list diff --git a/apps/opencs/view/settings/rangeview.cpp b/apps/opencs/view/settings/rangeview.cpp index 8ae6caca0..246f7ece2 100644 --- a/apps/opencs/view/settings/rangeview.cpp +++ b/apps/opencs/view/settings/rangeview.cpp @@ -36,8 +36,11 @@ CSVSettings::RangeView::RangeView (CSMSettings::Setting *setting, break; } - mRangeWidget->setFixedWidth (widgetWidth (setting->widgetWidth())); - mRangeWidget->setObjectName (setting->name()); + if(mRangeWidget) + { + mRangeWidget->setFixedWidth (widgetWidth (setting->widgetWidth())); + mRangeWidget->setObjectName (setting->name()); + } addWidget (mRangeWidget); } @@ -75,13 +78,16 @@ void CSVSettings::RangeView::buildSlider (CSMSettings::Setting *setting) break; } - mRangeWidget->setProperty ("minimum", setting->minimum()); - mRangeWidget->setProperty ("maximum", setting->maximum()); - mRangeWidget->setProperty ("tracking", false); - mRangeWidget->setProperty ("singleStep", setting->singleStep()); + if(mRangeWidget) + { + mRangeWidget->setProperty ("minimum", setting->minimum()); + mRangeWidget->setProperty ("maximum", setting->maximum()); + mRangeWidget->setProperty ("tracking", false); + mRangeWidget->setProperty ("singleStep", setting->singleStep()); - connect (mRangeWidget, SIGNAL (valueChanged (int)), - this, SLOT (slotUpdateView (int))); + connect (mRangeWidget, SIGNAL (valueChanged (int)), + this, SLOT (slotUpdateView (int))); + } } void CSVSettings::RangeView::buildSpinBox (CSMSettings::Setting *setting) @@ -111,7 +117,7 @@ void CSVSettings::RangeView::buildSpinBox (CSMSettings::Setting *setting) break; default: - break; + return; } //min / max values are set automatically in AlphaSpinBox @@ -120,14 +126,15 @@ void CSVSettings::RangeView::buildSpinBox (CSMSettings::Setting *setting) mRangeWidget->setProperty ("minimum", setting->minimum()); mRangeWidget->setProperty ("maximum", setting->maximum()); mRangeWidget->setProperty ("singleStep", setting->singleStep()); - mRangeWidget->setProperty ("specialValueText", - setting->specialValueText()); } mRangeWidget->setProperty ("prefix", setting->prefix()); mRangeWidget->setProperty ("suffix", setting->suffix()); mRangeWidget->setProperty ("wrapping", setting->wrapping()); + dynamic_cast (mRangeWidget)->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter); + if(setting->type() == CSMSettings::Type_SpinBox && setting->declaredValues().isEmpty()) + dynamic_cast (mRangeWidget)->setValue (setting->defaultValues().at(0).toInt()); } void CSVSettings::RangeView::slotUpdateView (int value) diff --git a/apps/opencs/view/settings/resizeablestackedwidget.cpp b/apps/opencs/view/settings/resizeablestackedwidget.cpp index cb127cb76..0e87a2506 100644 --- a/apps/opencs/view/settings/resizeablestackedwidget.cpp +++ b/apps/opencs/view/settings/resizeablestackedwidget.cpp @@ -34,7 +34,6 @@ void CSVSettings::ResizeableStackedWidget::changePage curPage->showWidgets(); layout()->activate(); - setFixedSize(minimumSizeHint()); setCurrentIndex (current); } diff --git a/apps/opencs/view/settings/settingwindow.cpp b/apps/opencs/view/settings/settingwindow.cpp index 7cdf2bded..76ea9dc4f 100644 --- a/apps/opencs/view/settings/settingwindow.cpp +++ b/apps/opencs/view/settings/settingwindow.cpp @@ -9,7 +9,7 @@ #include "view.hpp" CSVSettings::SettingWindow::SettingWindow(QWidget *parent) - : QMainWindow(parent) + : QMainWindow(parent), mModel(NULL) {} void CSVSettings::SettingWindow::createPages() @@ -19,10 +19,10 @@ void CSVSettings::SettingWindow::createPages() QList connectedSettings; foreach (const QString &pageName, pageMap.keys()) - { - QList pageSettings = pageMap.value (pageName); + { + QList pageSettings = pageMap.value (pageName).second; - mPages.append (new Page (pageName, pageSettings, this)); + mPages.append (new Page (pageName, pageSettings, this, pageMap.value (pageName).first)); for (int i = 0; i < pageSettings.size(); i++) { @@ -84,7 +84,7 @@ void CSVSettings::SettingWindow::createConnections void CSVSettings::SettingWindow::setViewValues() { - //iterate each page and view, setting their definintions + //iterate each page and view, setting their definitions //if they exist in the model foreach (const Page *page, mPages) { @@ -129,7 +129,3 @@ void CSVSettings::SettingWindow::saveSettings() mModel->saveDefinitions(); } -void CSVSettings::SettingWindow::closeEvent (QCloseEvent *event) -{ - QApplication::focusWidget()->clearFocus(); -} diff --git a/apps/opencs/view/settings/settingwindow.hpp b/apps/opencs/view/settings/settingwindow.hpp index 2266f130d..11bceee96 100644 --- a/apps/opencs/view/settings/settingwindow.hpp +++ b/apps/opencs/view/settings/settingwindow.hpp @@ -36,8 +36,6 @@ namespace CSVSettings { protected: - virtual void closeEvent (QCloseEvent *event); - ///construct the pages to be displayed in the dialog void createPages(); diff --git a/apps/opencs/view/settings/spinbox.cpp b/apps/opencs/view/settings/spinbox.cpp index 4b1447f8f..c70fc36d1 100644 --- a/apps/opencs/view/settings/spinbox.cpp +++ b/apps/opencs/view/settings/spinbox.cpp @@ -24,7 +24,7 @@ QString CSVSettings::SpinBox::textFromValue(int val) const int CSVSettings::SpinBox::valueFromText(const QString &text) const { if (mValueList.isEmpty()) - return -1; + return text.toInt(); // TODO: assumed integer, untested error handling for alpha types if (mValueList.contains (text)) return mValueList.indexOf(text); diff --git a/apps/opencs/view/settings/view.cpp b/apps/opencs/view/settings/view.cpp index 69109e2b3..39c7f89b2 100644 --- a/apps/opencs/view/settings/view.cpp +++ b/apps/opencs/view/settings/view.cpp @@ -17,11 +17,17 @@ CSVSettings::View::View(CSMSettings::Setting *setting, mIsMultiValue (setting->isMultiValue()), mViewKey (setting->page() + '/' + setting->name()), mSerializable (setting->serializable()), - Frame(true, setting->name(), parent) + Frame(true, setting->getLabel(), parent) { + if (!setting->getToolTip().isEmpty()) + setToolTip (setting->getToolTip()); + setObjectName (setting->name()); buildView(); buildModel (setting); + // apply stylesheet to view's frame if exists + if(setting->styleSheet() != "") + Frame::setStyleSheet (setting->styleSheet()); } void CSVSettings::View::buildModel (const CSMSettings::Setting *setting) diff --git a/apps/opencs/view/tools/reportsubview.cpp b/apps/opencs/view/tools/reportsubview.cpp index e84f5cf4b..df1a5298c 100644 --- a/apps/opencs/view/tools/reportsubview.cpp +++ b/apps/opencs/view/tools/reportsubview.cpp @@ -1,31 +1,15 @@ #include "reportsubview.hpp" -#include -#include - -#include "../../model/tools/reportmodel.hpp" - -#include "../../view/world/idtypedelegate.hpp" +#include "reporttable.hpp" CSVTools::ReportSubView::ReportSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document) -: CSVDoc::SubView (id), mModel (document.getReport (id)) +: CSVDoc::SubView (id) { - setWidget (mTable = new QTableView (this)); - mTable->setModel (mModel); - - mTable->horizontalHeader()->setResizeMode (QHeaderView::Interactive); - mTable->verticalHeader()->hide(); - mTable->setSortingEnabled (true); - mTable->setSelectionBehavior (QAbstractItemView::SelectRows); - mTable->setSelectionMode (QAbstractItemView::ExtendedSelection); - - mIdTypeDelegate = CSVWorld::IdTypeDelegateFactory().makeDelegate ( - document.getUndoStack(), this); + setWidget (mTable = new ReportTable (document, id, this)); - mTable->setItemDelegateForColumn (0, mIdTypeDelegate); - - connect (mTable, SIGNAL (doubleClicked (const QModelIndex&)), this, SLOT (show (const QModelIndex&))); + connect (mTable, SIGNAL (editRequest (const CSMWorld::UniversalId&, const std::string&)), + SIGNAL (focusId (const CSMWorld::UniversalId&, const std::string&))); } void CSVTools::ReportSubView::setEditLock (bool locked) @@ -33,13 +17,7 @@ void CSVTools::ReportSubView::setEditLock (bool locked) // ignored. We don't change document state anyway. } -void CSVTools::ReportSubView::updateUserSetting - (const QString &name, const QStringList &list) -{ - mIdTypeDelegate->updateUserSetting (name, list); -} - -void CSVTools::ReportSubView::show (const QModelIndex& index) +void CSVTools::ReportSubView::updateUserSetting (const QString &name, const QStringList &list) { - focusId (mModel->getUniversalId (index.row()), ""); + mTable->updateUserSetting (name, list); } diff --git a/apps/opencs/view/tools/reportsubview.hpp b/apps/opencs/view/tools/reportsubview.hpp index 9f6a4c1da..7e8a08e3c 100644 --- a/apps/opencs/view/tools/reportsubview.hpp +++ b/apps/opencs/view/tools/reportsubview.hpp @@ -11,27 +11,15 @@ namespace CSMDoc class Document; } -namespace CSMTools -{ - class ReportModel; -} - -namespace CSVWorld -{ - class CommandDelegate; -} - namespace CSVTools { - class Table; + class ReportTable; class ReportSubView : public CSVDoc::SubView { Q_OBJECT - CSMTools::ReportModel *mModel; - QTableView *mTable; - CSVWorld::CommandDelegate *mIdTypeDelegate; + ReportTable *mTable; public: @@ -39,12 +27,7 @@ namespace CSVTools virtual void setEditLock (bool locked); - virtual void updateUserSetting - (const QString &, const QStringList &); - - private slots: - - void show (const QModelIndex& index); + virtual void updateUserSetting (const QString &, const QStringList &); }; } diff --git a/apps/opencs/view/tools/reporttable.cpp b/apps/opencs/view/tools/reporttable.cpp new file mode 100644 index 000000000..4cd11925e --- /dev/null +++ b/apps/opencs/view/tools/reporttable.cpp @@ -0,0 +1,136 @@ + +#include "reporttable.hpp" + +#include + +#include +#include +#include + +#include "../../model/tools/reportmodel.hpp" + +#include "../../view/world/idtypedelegate.hpp" + +void CSVTools::ReportTable::contextMenuEvent (QContextMenuEvent *event) +{ + QModelIndexList selectedRows = selectionModel()->selectedRows(); + + // create context menu + QMenu menu (this); + + if (!selectedRows.empty()) + { + menu.addAction (mShowAction); + menu.addAction (mRemoveAction); + } + + menu.exec (event->globalPos()); +} + +void CSVTools::ReportTable::mouseMoveEvent (QMouseEvent *event) +{ + if (event->buttons() & Qt::LeftButton) + startDrag (*this); +} + +void CSVTools::ReportTable::mouseDoubleClickEvent (QMouseEvent *event) +{ + Qt::KeyboardModifiers modifiers = + event->modifiers() & (Qt::ShiftModifier | Qt::ControlModifier); + + QModelIndex index = currentIndex(); + + selectionModel()->select (index, + QItemSelectionModel::Clear | QItemSelectionModel::Select | QItemSelectionModel::Rows); + + switch (modifiers) + { + case 0: + + event->accept(); + showSelection(); + break; + + case Qt::ShiftModifier: + + event->accept(); + removeSelection(); + break; + + case Qt::ControlModifier: + + event->accept(); + showSelection(); + removeSelection(); + break; + } +} + +CSVTools::ReportTable::ReportTable (CSMDoc::Document& document, + const CSMWorld::UniversalId& id, QWidget *parent) +: CSVWorld::DragRecordTable (document, parent), mModel (document.getReport (id)) +{ + horizontalHeader()->setResizeMode (QHeaderView::Interactive); + verticalHeader()->hide(); + setSortingEnabled (true); + setSelectionBehavior (QAbstractItemView::SelectRows); + setSelectionMode (QAbstractItemView::ExtendedSelection); + + setModel (mModel); + setColumnHidden (2, true); + + mIdTypeDelegate = CSVWorld::IdTypeDelegateFactory().makeDelegate ( + document, this); + + setItemDelegateForColumn (0, mIdTypeDelegate); + + mShowAction = new QAction (tr ("Show"), this); + connect (mShowAction, SIGNAL (triggered()), this, SLOT (showSelection())); + addAction (mShowAction); + + mRemoveAction = new QAction (tr ("Remove from list"), this); + connect (mRemoveAction, SIGNAL (triggered()), this, SLOT (removeSelection())); + addAction (mRemoveAction); +} + +std::vector CSVTools::ReportTable::getDraggedRecords() const +{ + std::vector ids; + + QModelIndexList selectedRows = selectionModel()->selectedRows(); + + for (QModelIndexList::const_iterator iter (selectedRows.begin()); iter!=selectedRows.end(); + ++iter) + { + ids.push_back (mModel->getUniversalId (iter->row())); + } + + return ids; +} + +void CSVTools::ReportTable::updateUserSetting (const QString& name, const QStringList& list) +{ + mIdTypeDelegate->updateUserSetting (name, list); +} + +void CSVTools::ReportTable::showSelection() +{ + QModelIndexList selectedRows = selectionModel()->selectedRows(); + + for (QModelIndexList::const_iterator iter (selectedRows.begin()); iter!=selectedRows.end(); + ++iter) + emit editRequest (mModel->getUniversalId (iter->row()), mModel->getHint (iter->row())); +} + +void CSVTools::ReportTable::removeSelection() +{ + QModelIndexList selectedRows = selectionModel()->selectedRows(); + + std::reverse (selectedRows.begin(), selectedRows.end()); + + for (QModelIndexList::const_iterator iter (selectedRows.begin()); iter!=selectedRows.end(); + ++iter) + mModel->removeRows (iter->row(), 1); + + selectionModel()->clear(); +} \ No newline at end of file diff --git a/apps/opencs/view/tools/reporttable.hpp b/apps/opencs/view/tools/reporttable.hpp new file mode 100644 index 000000000..7a5b232f9 --- /dev/null +++ b/apps/opencs/view/tools/reporttable.hpp @@ -0,0 +1,58 @@ +#ifndef CSV_TOOLS_REPORTTABLE_H +#define CSV_TOOLS_REPORTTABLE_H + +#include "../world/dragrecordtable.hpp" + +class QAction; + +namespace CSMTools +{ + class ReportModel; +} + +namespace CSVWorld +{ + class CommandDelegate; +} + +namespace CSVTools +{ + class ReportTable : public CSVWorld::DragRecordTable + { + Q_OBJECT + + CSMTools::ReportModel *mModel; + CSVWorld::CommandDelegate *mIdTypeDelegate; + QAction *mShowAction; + QAction *mRemoveAction; + + private: + + void contextMenuEvent (QContextMenuEvent *event); + + void mouseMoveEvent (QMouseEvent *event); + + virtual void mouseDoubleClickEvent (QMouseEvent *event); + + public: + + ReportTable (CSMDoc::Document& document, const CSMWorld::UniversalId& id, + QWidget *parent = 0); + + virtual std::vector getDraggedRecords() const; + + void updateUserSetting (const QString& name, const QStringList& list); + + private slots: + + void showSelection(); + + void removeSelection(); + + signals: + + void editRequest (const CSMWorld::UniversalId& id, const std::string& hint); + }; +} + +#endif diff --git a/apps/opencs/view/widget/modebutton.cpp b/apps/opencs/view/widget/modebutton.cpp new file mode 100644 index 000000000..56896b422 --- /dev/null +++ b/apps/opencs/view/widget/modebutton.cpp @@ -0,0 +1,10 @@ + +#include "modebutton.hpp" + +CSVWidget::ModeButton::ModeButton (const QIcon& icon, const QString& tooltip, QWidget *parent) +: PushButton (icon, Type_Mode, tooltip, parent) +{} + +void CSVWidget::ModeButton::activate (SceneToolbar *toolbar) {} + +void CSVWidget::ModeButton::deactivate (SceneToolbar *toolbar) {} diff --git a/apps/opencs/view/widget/modebutton.hpp b/apps/opencs/view/widget/modebutton.hpp new file mode 100644 index 000000000..ac14afc95 --- /dev/null +++ b/apps/opencs/view/widget/modebutton.hpp @@ -0,0 +1,28 @@ +#ifndef CSV_WIDGET_MODEBUTTON_H +#define CSV_WIDGET_MODEBUTTON_H + +#include "pushbutton.hpp" + +namespace CSVWidget +{ + class SceneToolbar; + + /// \brief Specialist PushButton of Type_Mode for use in SceneToolMode + class ModeButton : public PushButton + { + Q_OBJECT + + public: + + ModeButton (const QIcon& icon, const QString& tooltip = "", + QWidget *parent = 0); + + /// Default-Implementation: do nothing + virtual void activate (SceneToolbar *toolbar); + + /// Default-Implementation: do nothing + virtual void deactivate (SceneToolbar *toolbar); + }; +} + +#endif diff --git a/apps/opencs/view/widget/pushbutton.cpp b/apps/opencs/view/widget/pushbutton.cpp index 056e548fb..d4e600794 100644 --- a/apps/opencs/view/widget/pushbutton.cpp +++ b/apps/opencs/view/widget/pushbutton.cpp @@ -4,9 +4,9 @@ #include #include -void CSVWidget::PushButton::setExtendedToolTip (const QString& text) +void CSVWidget::PushButton::setExtendedToolTip() { - QString tooltip = text; + QString tooltip = mToolTip; if (tooltip.isEmpty()) tooltip = "(Tool tip not implemented yet)"; @@ -20,6 +20,10 @@ void CSVWidget::PushButton::setExtendedToolTip (const QString& text) break; + case Type_TopAction: + + break; + case Type_Mode: tooltip += @@ -27,6 +31,16 @@ void CSVWidget::PushButton::setExtendedToolTip (const QString& text) "
    shift-left click to activate and keep panel open)"; break; + + case Type_Toggle: + + tooltip += "

    (left click to "; + tooltip += isChecked() ? "disable" : "enable"; + tooltip += "

    shift-left click to "; + tooltip += isChecked() ? "disable" : "enable"; + tooltip += " and keep panel open)"; + + break; } setToolTip (tooltip); @@ -42,11 +56,8 @@ void CSVWidget::PushButton::keyPressEvent (QKeyEvent *event) void CSVWidget::PushButton::keyReleaseEvent (QKeyEvent *event) { - if (event->key()==Qt::Key_Return || event->key()==Qt::Key_Enter) - { + if (event->key()==Qt::Key_Space) mKeepOpen = event->modifiers() & Qt::ShiftModifier; - emit clicked(); - } QPushButton::keyReleaseEvent (event); } @@ -61,15 +72,20 @@ CSVWidget::PushButton::PushButton (const QIcon& icon, Type type, const QString& QWidget *parent) : QPushButton (icon, "", parent), mKeepOpen (false), mType (type), mToolTip (tooltip) { - setCheckable (type==Type_Mode); - setExtendedToolTip (tooltip); + if (type==Type_Mode || type==Type_Toggle) + { + setCheckable (true); + connect (this, SIGNAL (toggled (bool)), this, SLOT (checkedStateChanged (bool))); + } + setCheckable (type==Type_Mode || type==Type_Toggle); + setExtendedToolTip(); } CSVWidget::PushButton::PushButton (Type type, const QString& tooltip, QWidget *parent) : QPushButton (parent), mKeepOpen (false), mType (type), mToolTip (tooltip) { - setCheckable (type==Type_Mode); - setExtendedToolTip (tooltip); + setCheckable (type==Type_Mode || type==Type_Toggle); + setExtendedToolTip(); } bool CSVWidget::PushButton::hasKeepOpen() const @@ -80,4 +96,14 @@ bool CSVWidget::PushButton::hasKeepOpen() const QString CSVWidget::PushButton::getBaseToolTip() const { return mToolTip; +} + +CSVWidget::PushButton::Type CSVWidget::PushButton::getType() const +{ + return mType; +} + +void CSVWidget::PushButton::checkedStateChanged (bool checked) +{ + setExtendedToolTip(); } \ No newline at end of file diff --git a/apps/opencs/view/widget/pushbutton.hpp b/apps/opencs/view/widget/pushbutton.hpp index 9b90bd0c6..09cf22757 100644 --- a/apps/opencs/view/widget/pushbutton.hpp +++ b/apps/opencs/view/widget/pushbutton.hpp @@ -14,7 +14,9 @@ namespace CSVWidget enum Type { Type_TopMode, // top level button for mode selector panel - Type_Mode // mode button + Type_TopAction, // top level button that triggers an action + Type_Mode, // mode button + Type_Toggle }; private: @@ -25,7 +27,7 @@ namespace CSVWidget private: - void setExtendedToolTip (const QString& text); + void setExtendedToolTip(); protected: @@ -49,6 +51,12 @@ namespace CSVWidget /// Return tooltip used at construction (without any button-specific modifications) QString getBaseToolTip() const; + + Type getType() const; + + private slots: + + void checkedStateChanged (bool checked); }; } diff --git a/apps/opencs/view/widget/scenetool.cpp b/apps/opencs/view/widget/scenetool.cpp index e3f2dfd1c..b8e9f895f 100644 --- a/apps/opencs/view/widget/scenetool.cpp +++ b/apps/opencs/view/widget/scenetool.cpp @@ -1,10 +1,12 @@ #include "scenetool.hpp" +#include + #include "scenetoolbar.hpp" -CSVWidget::SceneTool::SceneTool (SceneToolbar *parent) -: PushButton (PushButton::Type_TopMode, "", parent) +CSVWidget::SceneTool::SceneTool (SceneToolbar *parent, Type type) +: PushButton (type, "", parent) { setSizePolicy (QSizePolicy (QSizePolicy::Fixed, QSizePolicy::Fixed)); setIconSize (QSize (parent->getIconSize(), parent->getIconSize())); @@ -13,7 +15,20 @@ CSVWidget::SceneTool::SceneTool (SceneToolbar *parent) connect (this, SIGNAL (clicked()), this, SLOT (openRequest())); } +void CSVWidget::SceneTool::activate() {} + +void CSVWidget::SceneTool::mouseReleaseEvent (QMouseEvent *event) +{ + if (getType()==Type_TopAction && event->button()==Qt::RightButton) + showPanel (parentWidget()->mapToGlobal (pos())); + else + PushButton::mouseReleaseEvent (event); +} + void CSVWidget::SceneTool::openRequest() { - showPanel (parentWidget()->mapToGlobal (pos())); + if (getType()==Type_TopAction) + activate(); + else + showPanel (parentWidget()->mapToGlobal (pos())); } diff --git a/apps/opencs/view/widget/scenetool.hpp b/apps/opencs/view/widget/scenetool.hpp index 24099683e..cdea88096 100644 --- a/apps/opencs/view/widget/scenetool.hpp +++ b/apps/opencs/view/widget/scenetool.hpp @@ -14,10 +14,18 @@ namespace CSVWidget public: - SceneTool (SceneToolbar *parent); + SceneTool (SceneToolbar *parent, Type type = Type_TopMode); virtual void showPanel (const QPoint& position) = 0; + /// This function will only called for buttons of type Type_TopAction. The default + /// implementation is empty. + virtual void activate(); + + protected: + + void mouseReleaseEvent (QMouseEvent *event); + private slots: void openRequest(); diff --git a/apps/opencs/view/widget/scenetoolbar.cpp b/apps/opencs/view/widget/scenetoolbar.cpp index eac9bcec3..f7023b31f 100644 --- a/apps/opencs/view/widget/scenetoolbar.cpp +++ b/apps/opencs/view/widget/scenetoolbar.cpp @@ -31,9 +31,20 @@ CSVWidget::SceneToolbar::SceneToolbar (int buttonSize, QWidget *parent) connect (focusScene, SIGNAL (activated()), this, SIGNAL (focusSceneRequest())); } -void CSVWidget::SceneToolbar::addTool (SceneTool *tool) +void CSVWidget::SceneToolbar::addTool (SceneTool *tool, SceneTool *insertPoint) { - mLayout->addWidget (tool, 0, Qt::AlignTop); + if (!insertPoint) + mLayout->addWidget (tool, 0, Qt::AlignTop); + else + { + int index = mLayout->indexOf (insertPoint); + mLayout->insertWidget (index+1, tool, 0, Qt::AlignTop); + } +} + +void CSVWidget::SceneToolbar::removeTool (SceneTool *tool) +{ + mLayout->removeWidget (tool); } int CSVWidget::SceneToolbar::getButtonSize() const diff --git a/apps/opencs/view/widget/scenetoolbar.hpp b/apps/opencs/view/widget/scenetoolbar.hpp index 1902ba2be..8e2c8ab00 100644 --- a/apps/opencs/view/widget/scenetoolbar.hpp +++ b/apps/opencs/view/widget/scenetoolbar.hpp @@ -25,7 +25,11 @@ namespace CSVWidget SceneToolbar (int buttonSize, QWidget *parent = 0); - void addTool (SceneTool *tool); + /// If insertPoint==0, insert \a tool at the end of the scrollbar. Otherwise + /// insert tool after \a insertPoint. + void addTool (SceneTool *tool, SceneTool *insertPoint = 0); + + void removeTool (SceneTool *tool); int getButtonSize() const; diff --git a/apps/opencs/view/widget/scenetoolmode.cpp b/apps/opencs/view/widget/scenetoolmode.cpp index caedfa3ee..8d871cc5f 100644 --- a/apps/opencs/view/widget/scenetoolmode.cpp +++ b/apps/opencs/view/widget/scenetoolmode.cpp @@ -6,9 +6,9 @@ #include #include "scenetoolbar.hpp" -#include "pushbutton.hpp" +#include "modebutton.hpp" -void CSVWidget::SceneToolMode::adjustToolTip (const PushButton *activeMode) +void CSVWidget::SceneToolMode::adjustToolTip (const ModeButton *activeMode) { QString toolTip = mToolTip; @@ -21,7 +21,7 @@ void CSVWidget::SceneToolMode::adjustToolTip (const PushButton *activeMode) CSVWidget::SceneToolMode::SceneToolMode (SceneToolbar *parent, const QString& toolTip) : SceneTool (parent), mButtonSize (parent->getButtonSize()), mIconSize (parent->getIconSize()), - mToolTip (toolTip), mFirst (0) + mToolTip (toolTip), mFirst (0), mCurrent (0), mToolbar (parent) { mPanel = new QFrame (this, Qt::Popup); @@ -44,8 +44,14 @@ void CSVWidget::SceneToolMode::showPanel (const QPoint& position) void CSVWidget::SceneToolMode::addButton (const std::string& icon, const std::string& id, const QString& tooltip) { - PushButton *button = new PushButton (QIcon (QPixmap (icon.c_str())), PushButton::Type_Mode, - tooltip, mPanel); + ModeButton *button = new ModeButton (QIcon (QPixmap (icon.c_str())), tooltip, mPanel); + addButton (button, id); +} + +void CSVWidget::SceneToolMode::addButton (ModeButton *button, const std::string& id) +{ + button->setParent (mPanel); + button->setSizePolicy (QSizePolicy (QSizePolicy::Fixed, QSizePolicy::Fixed)); button->setIconSize (QSize (mIconSize, mIconSize)); button->setFixedSize (mButtonSize, mButtonSize); @@ -58,29 +64,40 @@ void CSVWidget::SceneToolMode::addButton (const std::string& icon, const std::st if (mButtons.size()==1) { - mFirst = button; + mFirst = mCurrent = button; setIcon (button->icon()); button->setChecked (true); adjustToolTip (button); + mCurrent->activate (mToolbar); } } void CSVWidget::SceneToolMode::selected() { - std::map::const_iterator iter = - mButtons.find (dynamic_cast (sender())); + std::map::const_iterator iter = + mButtons.find (dynamic_cast (sender())); if (iter!=mButtons.end()) { if (!iter->first->hasKeepOpen()) mPanel->hide(); - for (std::map::const_iterator iter2 = mButtons.begin(); + for (std::map::const_iterator iter2 = mButtons.begin(); iter2!=mButtons.end(); ++iter2) iter2->first->setChecked (iter2==iter); setIcon (iter->first->icon()); adjustToolTip (iter->first); + + if (mCurrent!=iter->first) + { + if (mCurrent) + mCurrent->deactivate (mToolbar); + + mCurrent = iter->first; + mCurrent->activate (mToolbar); + } + emit modeChanged (iter->second); } } \ No newline at end of file diff --git a/apps/opencs/view/widget/scenetoolmode.hpp b/apps/opencs/view/widget/scenetoolmode.hpp index 9959f9835..e6f462cf8 100644 --- a/apps/opencs/view/widget/scenetoolmode.hpp +++ b/apps/opencs/view/widget/scenetoolmode.hpp @@ -10,7 +10,7 @@ class QHBoxLayout; namespace CSVWidget { class SceneToolbar; - class PushButton; + class ModeButton; ///< \brief Mode selector tool class SceneToolMode : public SceneTool @@ -19,13 +19,15 @@ namespace CSVWidget QWidget *mPanel; QHBoxLayout *mLayout; - std::map mButtons; // widget, id + std::map mButtons; // widget, id int mButtonSize; int mIconSize; QString mToolTip; PushButton *mFirst; + ModeButton *mCurrent; + SceneToolbar *mToolbar; - void adjustToolTip (const PushButton *activeMode); + void adjustToolTip (const ModeButton *activeMode); public: @@ -36,6 +38,9 @@ namespace CSVWidget void addButton (const std::string& icon, const std::string& id, const QString& tooltip = ""); + /// The ownership of \a button is transferred to *this. + void addButton (ModeButton *button, const std::string& id); + signals: void modeChanged (const std::string& id); diff --git a/apps/opencs/view/widget/scenetoolrun.cpp b/apps/opencs/view/widget/scenetoolrun.cpp new file mode 100644 index 000000000..0c7a4b9f0 --- /dev/null +++ b/apps/opencs/view/widget/scenetoolrun.cpp @@ -0,0 +1,151 @@ + +#include "scenetoolrun.hpp" + +#include + +#include +#include +#include +#include +#include + +void CSVWidget::SceneToolRun::adjustToolTips() +{ + QString toolTip = mToolTip; + + if (mSelected==mProfiles.end()) + toolTip += "

    No debug profile selected (function disabled)"; + else + { + toolTip += "

    Debug profile: " + QString::fromUtf8 (mSelected->c_str()); + toolTip += "

    (right click to switch to a different profile)"; + } + + setToolTip (toolTip); +} + +void CSVWidget::SceneToolRun::updateIcon() +{ + setDisabled (mSelected==mProfiles.end()); +} + +void CSVWidget::SceneToolRun::updatePanel() +{ + mTable->setRowCount (mProfiles.size()); + + int i = 0; + + for (std::set::const_iterator iter (mProfiles.begin()); iter!=mProfiles.end(); + ++iter, ++i) + { + mTable->setItem (i, 0, new QTableWidgetItem (QString::fromUtf8 (iter->c_str()))); + + mTable->setItem (i, 1, new QTableWidgetItem ( + QApplication::style()->standardIcon (QStyle::SP_TitleBarCloseButton), "")); + } +} + +CSVWidget::SceneToolRun::SceneToolRun (SceneToolbar *parent, const QString& toolTip, + const QString& icon, const std::vector& profiles) +: SceneTool (parent, Type_TopAction), mProfiles (profiles.begin(), profiles.end()), + mSelected (mProfiles.begin()), mToolTip (toolTip) +{ + setIcon (QIcon (icon)); + updateIcon(); + adjustToolTips(); + + mPanel = new QFrame (this, Qt::Popup); + + QHBoxLayout *layout = new QHBoxLayout (mPanel); + + layout->setContentsMargins (QMargins (0, 0, 0, 0)); + + mTable = new QTableWidget (0, 2, this); + + mTable->setShowGrid (false); + mTable->verticalHeader()->hide(); + mTable->horizontalHeader()->hide(); + mTable->horizontalHeader()->setResizeMode (0, QHeaderView::Stretch); + mTable->horizontalHeader()->setResizeMode (1, QHeaderView::ResizeToContents); + mTable->setSelectionMode (QAbstractItemView::NoSelection); + + layout->addWidget (mTable); + + connect (mTable, SIGNAL (clicked (const QModelIndex&)), + this, SLOT (clicked (const QModelIndex&))); +} + +void CSVWidget::SceneToolRun::showPanel (const QPoint& position) +{ + updatePanel(); + + mPanel->move (position); + mPanel->show(); +} + +void CSVWidget::SceneToolRun::activate() +{ + if (mSelected!=mProfiles.end()) + emit runRequest (*mSelected); +} + +void CSVWidget::SceneToolRun::removeProfile (const std::string& profile) +{ + std::set::iterator iter = mProfiles.find (profile); + + if (iter!=mProfiles.end()) + { + if (iter==mSelected) + { + if (iter!=mProfiles.begin()) + --mSelected; + else + ++mSelected; + } + + mProfiles.erase (iter); + + if (mSelected==mProfiles.end()) + updateIcon(); + + adjustToolTips(); + } +} + +void CSVWidget::SceneToolRun::addProfile (const std::string& profile) +{ + std::set::iterator iter = mProfiles.find (profile); + + if (iter==mProfiles.end()) + { + mProfiles.insert (profile); + + if (mSelected==mProfiles.end()) + { + mSelected = mProfiles.begin(); + updateIcon(); + } + + adjustToolTips(); + } +} + +void CSVWidget::SceneToolRun::clicked (const QModelIndex& index) +{ + if (index.column()==0) + { + // select profile + mSelected = mProfiles.begin(); + std::advance (mSelected, index.row()); + mPanel->hide(); + adjustToolTips(); + } + else if (index.column()==1) + { + // remove profile from list + std::set::iterator iter = mProfiles.begin(); + std::advance (iter, index.row()); + removeProfile (*iter); + updatePanel(); + } +} \ No newline at end of file diff --git a/apps/opencs/view/widget/scenetoolrun.hpp b/apps/opencs/view/widget/scenetoolrun.hpp new file mode 100644 index 000000000..dd035462f --- /dev/null +++ b/apps/opencs/view/widget/scenetoolrun.hpp @@ -0,0 +1,62 @@ +#ifndef CSV_WIDGET_SCENETOOLRUN_H +#define CSV_WIDGET_SCENETOOLRUN_H + +#include +#include + +#include "scenetool.hpp" + +class QFrame; +class QTableWidget; +class QModelIndex; + +namespace CSVWidget +{ + class SceneToolRun : public SceneTool + { + Q_OBJECT + + std::set mProfiles; + std::set::iterator mSelected; + QString mToolTip; + QFrame *mPanel; + QTableWidget *mTable; + + private: + + void adjustToolTips(); + + void updateIcon(); + + void updatePanel(); + + public: + + SceneToolRun (SceneToolbar *parent, const QString& toolTip, const QString& icon, + const std::vector& profiles); + + virtual void showPanel (const QPoint& position); + + virtual void activate(); + + /// \attention This function does not remove the profile from the profile selection + /// panel. + void removeProfile (const std::string& profile); + + /// \attention This function doe not add the profile to the profile selection + /// panel. This only happens when the panel is re-opened. + /// + /// \note Adding profiles that are already listed is a no-op. + void addProfile (const std::string& profile); + + private slots: + + void clicked (const QModelIndex& index); + + signals: + + void runRequest (const std::string& profile); + }; +} + +#endif diff --git a/apps/opencs/view/widget/scenetooltoggle.cpp b/apps/opencs/view/widget/scenetooltoggle.cpp new file mode 100644 index 000000000..07c448e45 --- /dev/null +++ b/apps/opencs/view/widget/scenetooltoggle.cpp @@ -0,0 +1,205 @@ + +#include "scenetooltoggle.hpp" + +#include + +#include +#include +#include +#include + +#include "scenetoolbar.hpp" +#include "pushbutton.hpp" + +void CSVWidget::SceneToolToggle::adjustToolTip() +{ + QString toolTip = mToolTip; + + toolTip += "

    Currently enabled: "; + + bool first = true; + + for (std::map::const_iterator iter (mButtons.begin()); + iter!=mButtons.end(); ++iter) + if (iter->first->isChecked()) + { + if (!first) + toolTip += ", "; + else + first = false; + + toolTip += iter->second.mName; + } + + if (first) + toolTip += "none"; + + toolTip += "

    (left click to alter selection)"; + + setToolTip (toolTip); +} + +void CSVWidget::SceneToolToggle::adjustIcon() +{ + unsigned int selection = getSelection(); + if (!selection) + setIcon (QIcon (QString::fromUtf8 (mEmptyIcon.c_str()))); + else + { + QPixmap pixmap (48, 48); + pixmap.fill (QColor (0, 0, 0, 0)); + + { + QPainter painter (&pixmap); + + for (std::map::const_iterator iter (mButtons.begin()); + iter!=mButtons.end(); ++iter) + if (iter->first->isChecked()) + { + painter.drawImage (getIconBox (iter->second.mIndex), + QImage (QString::fromUtf8 (iter->second.mSmallIcon.c_str()))); + } + } + + setIcon (pixmap); + } +} + +QRect CSVWidget::SceneToolToggle::getIconBox (int index) const +{ + // layout for a 3x3 grid + int xMax = 3; + int yMax = 3; + + // icon size + int xBorder = 1; + int yBorder = 1; + + int iconXSize = (mIconSize-xBorder*(xMax+1))/xMax; + int iconYSize = (mIconSize-yBorder*(yMax+1))/yMax; + + int y = index / xMax; + int x = index % xMax; + + int total = mButtons.size(); + + int actualYIcons = total/xMax; + + if (total % xMax) + ++actualYIcons; + + if (actualYIcons!=yMax) + { + // space out icons vertically, if there aren't enough to populate all rows + int diff = yMax - actualYIcons; + yBorder += (diff*(yBorder+iconXSize)) / (actualYIcons+1); + } + + if (y==actualYIcons-1) + { + // generating the last row of icons + int actualXIcons = total % xMax; + + if (actualXIcons) + { + // space out icons horizontally, if there aren't enough to fill the last row + int diff = xMax - actualXIcons; + + xBorder += (diff*(xBorder+iconXSize)) / (actualXIcons+1); + } + } + + return QRect ((iconXSize+xBorder)*x+xBorder, (iconYSize+yBorder)*y+yBorder, + iconXSize, iconYSize); +} + +CSVWidget::SceneToolToggle::SceneToolToggle (SceneToolbar *parent, const QString& toolTip, + const std::string& emptyIcon) +: SceneTool (parent), mEmptyIcon (emptyIcon), mButtonSize (parent->getButtonSize()), + mIconSize (parent->getIconSize()), mToolTip (toolTip), mFirst (0) +{ + mPanel = new QFrame (this, Qt::Popup); + + mLayout = new QHBoxLayout (mPanel); + + mLayout->setContentsMargins (QMargins (0, 0, 0, 0)); + + mPanel->setLayout (mLayout); +} + +void CSVWidget::SceneToolToggle::showPanel (const QPoint& position) +{ + mPanel->move (position); + mPanel->show(); + + if (mFirst) + mFirst->setFocus (Qt::OtherFocusReason); +} + +void CSVWidget::SceneToolToggle::addButton (const std::string& icon, unsigned int id, + const std::string& smallIcon, const QString& name, const QString& tooltip) +{ + if (mButtons.size()>=9) + throw std::runtime_error ("Exceeded number of buttons in toggle type tool"); + + PushButton *button = new PushButton (QIcon (QPixmap (icon.c_str())), + PushButton::Type_Toggle, tooltip.isEmpty() ? name: tooltip, mPanel); + + button->setSizePolicy (QSizePolicy (QSizePolicy::Fixed, QSizePolicy::Fixed)); + button->setIconSize (QSize (mIconSize, mIconSize)); + button->setFixedSize (mButtonSize, mButtonSize); + + mLayout->addWidget (button); + + ButtonDesc desc; + desc.mId = id; + desc.mSmallIcon = smallIcon; + desc.mName = name; + desc.mIndex = mButtons.size(); + + mButtons.insert (std::make_pair (button, desc)); + + connect (button, SIGNAL (clicked()), this, SLOT (selected())); + + if (mButtons.size()==1) + mFirst = button; +} + +unsigned int CSVWidget::SceneToolToggle::getSelection() const +{ + unsigned int selection = 0; + + for (std::map::const_iterator iter (mButtons.begin()); + iter!=mButtons.end(); ++iter) + if (iter->first->isChecked()) + selection |= iter->second.mId; + + return selection; +} + +void CSVWidget::SceneToolToggle::setSelection (unsigned int selection) +{ + for (std::map::iterator iter (mButtons.begin()); + iter!=mButtons.end(); ++iter) + iter->first->setChecked (selection & iter->second.mId); + + adjustToolTip(); + adjustIcon(); +} + +void CSVWidget::SceneToolToggle::selected() +{ + std::map::const_iterator iter = + mButtons.find (dynamic_cast (sender())); + + if (iter!=mButtons.end()) + { + if (!iter->first->hasKeepOpen()) + mPanel->hide(); + + adjustToolTip(); + adjustIcon(); + + emit selectionChanged(); + } +} diff --git a/apps/opencs/view/widget/scenetooltoggle.hpp b/apps/opencs/view/widget/scenetooltoggle.hpp new file mode 100644 index 000000000..55e697524 --- /dev/null +++ b/apps/opencs/view/widget/scenetooltoggle.hpp @@ -0,0 +1,75 @@ +#ifndef CSV_WIDGET_SCENETOOL_TOGGLE_H +#define CSV_WIDGET_SCENETOOL_TOGGLE_H + +#include "scenetool.hpp" + +#include + +class QHBoxLayout; +class QRect; + +namespace CSVWidget +{ + class SceneToolbar; + class PushButton; + + ///< \brief Multi-Toggle tool + class SceneToolToggle : public SceneTool + { + Q_OBJECT + + struct ButtonDesc + { + unsigned int mId; + std::string mSmallIcon; + QString mName; + int mIndex; + }; + + std::string mEmptyIcon; + QWidget *mPanel; + QHBoxLayout *mLayout; + std::map mButtons; // widget, id + int mButtonSize; + int mIconSize; + QString mToolTip; + PushButton *mFirst; + + void adjustToolTip(); + + void adjustIcon(); + + QRect getIconBox (int index) const; + + public: + + SceneToolToggle (SceneToolbar *parent, const QString& toolTip, + const std::string& emptyIcon); + + virtual void showPanel (const QPoint& position); + + /// \attention After the last button has been added, setSelection must be called at + /// least once to finalise the layout. + /// + /// \note The layout algorithm can not handle more than 9 buttons. To prevent this An + /// attempt to add more will result in an exception being thrown. + /// The small icons will be sized at (x-4)/3 (where x is the main icon size). + void addButton (const std::string& icon, unsigned int id, + const std::string& smallIcon, const QString& name, const QString& tooltip = ""); + + unsigned int getSelection() const; + + /// \param or'ed button IDs. IDs that do not exist will be ignored. + void setSelection (unsigned int selection); + + signals: + + void selectionChanged(); + + private slots: + + void selected(); + }; +} + +#endif diff --git a/apps/opencs/view/widget/scenetooltoggle2.cpp b/apps/opencs/view/widget/scenetooltoggle2.cpp new file mode 100644 index 000000000..313e519cb --- /dev/null +++ b/apps/opencs/view/widget/scenetooltoggle2.cpp @@ -0,0 +1,142 @@ + +#include "scenetooltoggle2.hpp" + +#include +#include + +#include +#include +#include +#include + +#include "scenetoolbar.hpp" +#include "pushbutton.hpp" + +void CSVWidget::SceneToolToggle2::adjustToolTip() +{ + QString toolTip = mToolTip; + + toolTip += "

    Currently enabled: "; + + bool first = true; + + for (std::map::const_iterator iter (mButtons.begin()); + iter!=mButtons.end(); ++iter) + if (iter->first->isChecked()) + { + if (!first) + toolTip += ", "; + else + first = false; + + toolTip += iter->second.mName; + } + + if (first) + toolTip += "none"; + + toolTip += "

    (left click to alter selection)"; + + setToolTip (toolTip); +} + +void CSVWidget::SceneToolToggle2::adjustIcon() +{ + std::ostringstream stream; + stream << mCompositeIcon << getSelection(); + setIcon (QIcon (QString::fromUtf8 (stream.str().c_str()))); +} + +CSVWidget::SceneToolToggle2::SceneToolToggle2 (SceneToolbar *parent, const QString& toolTip, + const std::string& compositeIcon, const std::string& singleIcon) +: SceneTool (parent), mCompositeIcon (compositeIcon), mSingleIcon (singleIcon), + mButtonSize (parent->getButtonSize()), mIconSize (parent->getIconSize()), mToolTip (toolTip), + mFirst (0) +{ + mPanel = new QFrame (this, Qt::Popup); + + mLayout = new QHBoxLayout (mPanel); + + mLayout->setContentsMargins (QMargins (0, 0, 0, 0)); + + mPanel->setLayout (mLayout); +} + +void CSVWidget::SceneToolToggle2::showPanel (const QPoint& position) +{ + mPanel->move (position); + mPanel->show(); + + if (mFirst) + mFirst->setFocus (Qt::OtherFocusReason); +} + +void CSVWidget::SceneToolToggle2::addButton (unsigned int id, + const QString& name, const QString& tooltip, bool disabled) +{ + std::ostringstream stream; + stream << mSingleIcon << id; + + PushButton *button = new PushButton (QIcon (QPixmap (stream.str().c_str())), + PushButton::Type_Toggle, tooltip.isEmpty() ? name: tooltip, mPanel); + + button->setSizePolicy (QSizePolicy (QSizePolicy::Fixed, QSizePolicy::Fixed)); + button->setIconSize (QSize (mIconSize, mIconSize)); + button->setFixedSize (mButtonSize, mButtonSize); + + if (disabled) + button->setDisabled (true); + + mLayout->addWidget (button); + + ButtonDesc desc; + desc.mId = id; + desc.mName = name; + desc.mIndex = mButtons.size(); + + mButtons.insert (std::make_pair (button, desc)); + + connect (button, SIGNAL (clicked()), this, SLOT (selected())); + + if (mButtons.size()==1 && !disabled) + mFirst = button; +} + +unsigned int CSVWidget::SceneToolToggle2::getSelection() const +{ + unsigned int selection = 0; + + for (std::map::const_iterator iter (mButtons.begin()); + iter!=mButtons.end(); ++iter) + if (iter->first->isChecked()) + selection |= iter->second.mId; + + return selection; +} + +void CSVWidget::SceneToolToggle2::setSelection (unsigned int selection) +{ + for (std::map::iterator iter (mButtons.begin()); + iter!=mButtons.end(); ++iter) + iter->first->setChecked (selection & iter->second.mId); + + adjustToolTip(); + adjustIcon(); +} + +void CSVWidget::SceneToolToggle2::selected() +{ + std::map::const_iterator iter = + mButtons.find (dynamic_cast (sender())); + + if (iter!=mButtons.end()) + { + if (!iter->first->hasKeepOpen()) + mPanel->hide(); + + adjustToolTip(); + adjustIcon(); + + emit selectionChanged(); + } +} diff --git a/apps/opencs/view/widget/scenetooltoggle2.hpp b/apps/opencs/view/widget/scenetooltoggle2.hpp new file mode 100644 index 000000000..0bae780f9 --- /dev/null +++ b/apps/opencs/view/widget/scenetooltoggle2.hpp @@ -0,0 +1,76 @@ +#ifndef CSV_WIDGET_SCENETOOL_TOGGLE2_H +#define CSV_WIDGET_SCENETOOL_TOGGLE2_H + +#include "scenetool.hpp" + +#include + +class QHBoxLayout; +class QRect; + +namespace CSVWidget +{ + class SceneToolbar; + class PushButton; + + ///< \brief Multi-Toggle tool + /// + /// Top level button is using predefined icons instead building a composite icon. + class SceneToolToggle2 : public SceneTool + { + Q_OBJECT + + struct ButtonDesc + { + unsigned int mId; + QString mName; + int mIndex; + }; + + std::string mCompositeIcon; + std::string mSingleIcon; + QWidget *mPanel; + QHBoxLayout *mLayout; + std::map mButtons; // widget, id + int mButtonSize; + int mIconSize; + QString mToolTip; + PushButton *mFirst; + + void adjustToolTip(); + + void adjustIcon(); + + public: + + /// The top level icon is compositeIcon + sum of bitpatterns for active buttons (in + /// decimal) + /// + /// The icon for individual toggle buttons is signleIcon + bitmask for button (in + /// decimal) + SceneToolToggle2 (SceneToolbar *parent, const QString& toolTip, + const std::string& compositeIcon, const std::string& singleIcon); + + virtual void showPanel (const QPoint& position); + + /// \attention After the last button has been added, setSelection must be called at + /// least once to finalise the layout. + void addButton (unsigned int id, + const QString& name, const QString& tooltip = "", bool disabled = false); + + unsigned int getSelection() const; + + /// \param or'ed button IDs. IDs that do not exist will be ignored. + void setSelection (unsigned int selection); + + signals: + + void selectionChanged(); + + private slots: + + void selected(); + }; +} + +#endif diff --git a/apps/opencs/view/world/creator.cpp b/apps/opencs/view/world/creator.cpp index d753a2b47..a24c58e54 100644 --- a/apps/opencs/view/world/creator.cpp +++ b/apps/opencs/view/world/creator.cpp @@ -1,7 +1,16 @@ #include "creator.hpp" -CSVWorld::Creator:: ~Creator() {} +#include + +CSVWorld::Creator::~Creator() {} + +void CSVWorld::Creator::setScope (unsigned int scope) +{ + if (scope!=CSMWorld::Scope_Content) + throw std::logic_error ("Invalid scope in creator"); +} + CSVWorld::CreatorFactoryBase::~CreatorFactoryBase() {} diff --git a/apps/opencs/view/world/creator.hpp b/apps/opencs/view/world/creator.hpp index 88da70330..8e50e8715 100644 --- a/apps/opencs/view/world/creator.hpp +++ b/apps/opencs/view/world/creator.hpp @@ -1,9 +1,14 @@ #ifndef CSV_WORLD_CREATOR_H #define CSV_WORLD_CREATOR_H +#include + #include + #include "../../model/world/universalid.hpp" +#include "../../model/world/scope.hpp" + class QUndoStack; namespace CSMWorld @@ -32,6 +37,9 @@ namespace CSVWorld virtual void toggleWidgets(bool active = true) = 0; + /// Default implementation: Throw an exception if scope!=Scope_Content. + virtual void setScope (unsigned int scope); + signals: void done(); @@ -68,7 +76,7 @@ namespace CSVWorld /// \note The function always returns 0. }; - template + template class CreatorFactory : public CreatorFactoryBase { public: @@ -81,11 +89,15 @@ namespace CSVWorld /// records should be provided. }; - template - Creator *CreatorFactory::makeCreator (CSMWorld::Data& data, QUndoStack& undoStack, + template + Creator *CreatorFactory::makeCreator (CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id) const { - return new CreatorT (data, undoStack, id); + std::auto_ptr creator (new CreatorT (data, undoStack, id)); + + creator->setScope (scope); + + return creator.release(); } } diff --git a/apps/opencs/view/world/datadisplaydelegate.cpp b/apps/opencs/view/world/datadisplaydelegate.cpp index 31ec18d52..46ca17a29 100644 --- a/apps/opencs/view/world/datadisplaydelegate.cpp +++ b/apps/opencs/view/world/datadisplaydelegate.cpp @@ -6,11 +6,11 @@ CSVWorld::DataDisplayDelegate::DataDisplayDelegate(const ValueList &values, const IconList &icons, - QUndoStack &undoStack, + CSMDoc::Document& document, const QString &pageName, const QString &settingName, QObject *parent) - : EnumDelegate (values, undoStack, parent), mDisplayMode (Mode_TextOnly), + : EnumDelegate (values, document, parent), mDisplayMode (Mode_TextOnly), mIcons (icons), mIconSize (QSize(16, 16)), mIconLeftOffset(3), mTextLeftOffset(8), mSettingKey (pageName + '/' + settingName) { @@ -38,7 +38,7 @@ void CSVWorld::DataDisplayDelegate::buildPixmaps () } } -void CSVWorld::DataDisplayDelegate::setIconSize(const QSize size) +void CSVWorld::DataDisplayDelegate::setIconSize(const QSize& size) { mIconSize = size; buildPixmaps(); @@ -126,8 +126,6 @@ void CSVWorld::DataDisplayDelegate::updateDisplayMode (const QString &mode) CSVWorld::DataDisplayDelegate::~DataDisplayDelegate() { - mIcons.clear(); - mPixmaps.clear(); } void CSVWorld::DataDisplayDelegateFactory::add (int enumValue, QString enumName, QString iconFilename) @@ -137,11 +135,10 @@ void CSVWorld::DataDisplayDelegateFactory::add (int enumValue, QString enumName, } -CSVWorld::CommandDelegate *CSVWorld::DataDisplayDelegateFactory::makeDelegate (QUndoStack& undoStack, - QObject *parent) const +CSVWorld::CommandDelegate *CSVWorld::DataDisplayDelegateFactory::makeDelegate ( + CSMDoc::Document& document, QObject *parent) const { - - return new DataDisplayDelegate (mValues, mIcons, undoStack, "", "", parent); + return new DataDisplayDelegate (mValues, mIcons, document, "", "", parent); } diff --git a/apps/opencs/view/world/datadisplaydelegate.hpp b/apps/opencs/view/world/datadisplaydelegate.hpp index ef453c58f..73790e3c6 100755 --- a/apps/opencs/view/world/datadisplaydelegate.hpp +++ b/apps/opencs/view/world/datadisplaydelegate.hpp @@ -40,7 +40,7 @@ namespace CSVWorld public: explicit DataDisplayDelegate (const ValueList & values, const IconList & icons, - QUndoStack& undoStack, + CSMDoc::Document& document, const QString &pageName, const QString &settingName, QObject *parent); @@ -50,7 +50,7 @@ namespace CSVWorld virtual void paint (QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const; /// pass a QSize defining height / width of icon. Default is QSize (16,16). - void setIconSize (const QSize icon); + void setIconSize (const QSize& icon); /// offset the horizontal position of the icon from the left edge of the cell. Default is 3 pixels. void setIconLeftOffset (int offset); @@ -82,7 +82,7 @@ namespace CSVWorld public: - virtual CommandDelegate *makeDelegate (QUndoStack& undoStack, QObject *parent) const; + virtual CommandDelegate *makeDelegate (CSMDoc::Document& document, QObject *parent) const; ///< The ownership of the returned CommandDelegate is transferred to the caller. protected: diff --git a/apps/opencs/view/world/dialoguesubview.cpp b/apps/opencs/view/world/dialoguesubview.cpp index c3be0f11b..f6ad79a6a 100644 --- a/apps/opencs/view/world/dialoguesubview.cpp +++ b/apps/opencs/view/world/dialoguesubview.cpp @@ -170,10 +170,10 @@ void CSVWorld::DialogueDelegateDispatcherProxy::tableMimeDataDropped(const std:: ==============================DialogueDelegateDispatcher========================================== */ -CSVWorld::DialogueDelegateDispatcher::DialogueDelegateDispatcher(QObject* parent, CSMWorld::IdTable* table, QUndoStack& undoStack) : +CSVWorld::DialogueDelegateDispatcher::DialogueDelegateDispatcher(QObject* parent, CSMWorld::IdTable* table, CSMDoc::Document& document) : mParent(parent), mTable(table), -mUndoStack(undoStack), +mDocument (document), mNotEditableDelegate(table, parent) { } @@ -185,7 +185,7 @@ CSVWorld::CommandDelegate* CSVWorld::DialogueDelegateDispatcher::makeDelegate(CS if (delegateIt == mDelegates.end()) { delegate = CommandDelegateFactoryCollection::get().makeDelegate ( - display, mUndoStack, mParent); + display, mDocument, mParent); mDelegates.insert(std::make_pair(display, delegate)); } else { @@ -271,7 +271,6 @@ QWidget* CSVWorld::DialogueDelegateDispatcher::makeEditor(CSMWorld::ColumnBase:: DialogueDelegateDispatcherProxy* proxy = new DialogueDelegateDispatcherProxy(editor, display); - bool skip = false; if (qobject_cast(editor)) { connect(editor, SIGNAL(editingFinished()), proxy, SLOT(editorDataCommited())); @@ -284,25 +283,21 @@ QWidget* CSVWorld::DialogueDelegateDispatcher::makeEditor(CSMWorld::ColumnBase:: skip = true; } - if(!skip && qobject_cast(editor)) + else if (qobject_cast(editor)) { connect(editor, SIGNAL(stateChanged(int)), proxy, SLOT(editorDataCommited())); - skip = true; } - if(!skip && qobject_cast(editor)) + else if (qobject_cast(editor)) { connect(editor, SIGNAL(textChanged()), proxy, SLOT(editorDataCommited())); - skip = true; } - if(!skip && qobject_cast(editor)) + else if (qobject_cast(editor)) { connect(editor, SIGNAL(currentIndexChanged (int)), proxy, SLOT(editorDataCommited())); - skip = true; } - if(!skip && qobject_cast(editor)) + else if (qobject_cast(editor)) { connect(editor, SIGNAL(editingFinished()), proxy, SLOT(editorDataCommited())); - skip = true; } //lisp cond pairs would be nice in the C++ connect(proxy, SIGNAL(editorDataCommited(QWidget*, const QModelIndex&, CSMWorld::ColumnBase::Display)), @@ -338,7 +333,7 @@ mDispatcher(this, table, undoStack), QScrollArea(parent), mWidgetMapper(NULL), mMainWidget(NULL), -mUndoStack(undoStack), +mDocument (document), mTable(table) { remake (row); @@ -442,7 +437,6 @@ void CSVWorld::EditWidget::remake(int row) mWidgetMapper->setCurrentModelIndex(mTable->index(row, 0)); - this->setMinimumWidth(325); /// \todo replace hardcoded value with a user setting this->setWidget(mMainWidget); this->setWidgetResizable(true); } diff --git a/apps/opencs/view/world/dialoguesubview.hpp b/apps/opencs/view/world/dialoguesubview.hpp index c1bc96211..3f48aef42 100644 --- a/apps/opencs/view/world/dialoguesubview.hpp +++ b/apps/opencs/view/world/dialoguesubview.hpp @@ -113,7 +113,7 @@ namespace CSVWorld CSMWorld::IdTable* mTable; - QUndoStack& mUndoStack; + CSMDoc::Document& mDocument; NotEditableSubDelegate mNotEditableDelegate; @@ -169,7 +169,7 @@ namespace CSVWorld DialogueDelegateDispatcher mDispatcher; QWidget* mMainWidget; CSMWorld::IdTable* mTable; - QUndoStack& mUndoStack; + CSMDoc::Document& mDocument; std::vector mNestedModels; //Plain, raw C pointers, deleted in the dtor public: diff --git a/apps/opencs/view/world/enumdelegate.cpp b/apps/opencs/view/world/enumdelegate.cpp index 16f38938d..7c305b1b6 100644 --- a/apps/opencs/view/world/enumdelegate.cpp +++ b/apps/opencs/view/world/enumdelegate.cpp @@ -35,8 +35,8 @@ void CSVWorld::EnumDelegate::addCommands (QAbstractItemModel *model, CSVWorld::EnumDelegate::EnumDelegate (const std::vector >& values, - QUndoStack& undoStack, QObject *parent) -: CommandDelegate (undoStack, parent), mValues (values) + CSMDoc::Document& document, QObject *parent) +: CommandDelegate (document, parent), mValues (values) { } @@ -139,10 +139,10 @@ CSVWorld::EnumDelegateFactory::EnumDelegateFactory (const std::vector >& values, - QUndoStack& undoStack, QObject *parent); + CSMDoc::Document& document, QObject *parent); virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem& option, @@ -64,7 +64,7 @@ namespace CSVWorld EnumDelegateFactory (const std::vector& names, bool allowNone = false); /// \param allowNone Use value of -1 for "none selected" (empty string) - virtual CommandDelegate *makeDelegate (QUndoStack& undoStack, QObject *parent) const; + virtual CommandDelegate *makeDelegate (CSMDoc::Document& document, QObject *parent) const; ///< The ownership of the returned CommandDelegate is transferred to the caller. void add (int value, const QString& name); diff --git a/apps/opencs/view/world/genericcreator.cpp b/apps/opencs/view/world/genericcreator.cpp index 31c216e2c..afa59bc45 100644 --- a/apps/opencs/view/world/genericcreator.cpp +++ b/apps/opencs/view/world/genericcreator.cpp @@ -7,6 +7,10 @@ #include #include #include +#include +#include + +#include #include "../../model/world/commands.hpp" #include "../../model/world/data.hpp" @@ -46,32 +50,94 @@ std::string CSVWorld::GenericCreator::getId() const void CSVWorld::GenericCreator::configureCreateCommand (CSMWorld::CreateCommand& command) const {} +void CSVWorld::GenericCreator::pushCommand (std::auto_ptr command, + const std::string& id) +{ + mUndoStack.push (command.release()); +} + CSMWorld::Data& CSVWorld::GenericCreator::getData() const { return mData; } +QUndoStack& CSVWorld::GenericCreator::getUndoStack() +{ + return mUndoStack; +} + const CSMWorld::UniversalId& CSVWorld::GenericCreator::getCollectionId() const { return mListId; } -CSVWorld::GenericCreator::GenericCreator (CSMWorld::Data& data, QUndoStack& undoStack, - const CSMWorld::UniversalId& id, bool relaxedIdRules): +std::string CSVWorld::GenericCreator::getNamespace() const +{ + CSMWorld::Scope scope = CSMWorld::Scope_Content; - mData (data), - mUndoStack (undoStack), - mListId (id), - mLocked (false), - mCloneMode(false), - mClonedType(CSMWorld::UniversalId::Type_None) + if (mScope) + { + scope = static_cast (mScope->itemData (mScope->currentIndex()).toInt()); + } + else + { + if (mScopes & CSMWorld::Scope_Project) + scope = CSMWorld::Scope_Project; + else if (mScopes & CSMWorld::Scope_Session) + scope = CSMWorld::Scope_Session; + } + switch (scope) + { + case CSMWorld::Scope_Content: return ""; + case CSMWorld::Scope_Project: return "project::"; + case CSMWorld::Scope_Session: return "session::"; + } + + return ""; +} + +void CSVWorld::GenericCreator::updateNamespace() +{ + std::string namespace_ = getNamespace(); + + mValidator->setNamespace (namespace_); + + int index = mId->text().indexOf ("::"); + + if (index==-1) + { + // no namespace in old text + mId->setText (QString::fromUtf8 (namespace_.c_str()) + mId->text()); + } + else + { + std::string oldNamespace = + Misc::StringUtils::lowerCase (mId->text().left (index).toUtf8().constData()); + + if (oldNamespace=="project" || oldNamespace=="session") + mId->setText (QString::fromUtf8 (namespace_.c_str()) + mId->text().mid (index+2)); + } +} + +void CSVWorld::GenericCreator::addScope (const QString& name, CSMWorld::Scope scope, + const QString& tooltip) +{ + mScope->addItem (name, static_cast (scope)); + mScope->setItemData (mScope->count()-1, tooltip, Qt::ToolTipRole); +} + +CSVWorld::GenericCreator::GenericCreator (CSMWorld::Data& data, QUndoStack& undoStack, + const CSMWorld::UniversalId& id, bool relaxedIdRules) +: mData (data), mUndoStack (undoStack), mListId (id), mLocked (false), mCloneMode (false), + mClonedType (CSMWorld::UniversalId::Type_None), mScopes (CSMWorld::Scope_Content), mScope (0), + mScopeLabel (0) { mLayout = new QHBoxLayout; mLayout->setContentsMargins (0, 0, 0, 0); mId = new QLineEdit; - mId->setValidator (new IdValidator (relaxedIdRules, this)); + mId->setValidator (mValidator = new IdValidator (relaxedIdRules, this)); mLayout->addWidget (mId, 1); mCreate = new QPushButton ("Create"); @@ -99,22 +165,17 @@ void CSVWorld::GenericCreator::reset() mCloneMode = false; mId->setText (""); update(); + updateNamespace(); } std::string CSVWorld::GenericCreator::getErrors() const { std::string errors; - std::string id = getId(); - - if (id.empty()) - { - errors = "Missing ID"; - } - else if (mData.hasId (id)) - { + if (!mId->hasAcceptableInput()) + errors = mValidator->getError(); + else if (mData.hasId (getId())) errors = "ID is already in use"; - } return errors; } @@ -128,29 +189,27 @@ void CSVWorld::GenericCreator::create() { if (!mLocked) { + std::string id = getId(); + + std::auto_ptr command; + if (mCloneMode) { - std::string id = getId(); - std::auto_ptr command (new CSMWorld::CloneCommand ( + command.reset (new CSMWorld::CloneCommand ( dynamic_cast (*mData.getTableModel(mListId)), mClonedId, id, mClonedType)); + } + else + { + command.reset (new CSMWorld::CreateCommand ( + dynamic_cast (*mData.getTableModel (mListId)), id)); - mUndoStack.push(command.release()); - - emit done(); - emit requestFocus(id); - } else { - std::string id = getId(); - - std::auto_ptr command (new CSMWorld::CreateCommand ( - dynamic_cast (*mData.getTableModel (mListId)), id)); - - configureCreateCommand (*command); + } - mUndoStack.push (command.release()); + configureCreateCommand (*command); + pushCommand (command, id); - emit done(); - emit requestFocus (id); - } + emit done(); + emit requestFocus(id); } } @@ -165,3 +224,56 @@ void CSVWorld::GenericCreator::cloneMode(const std::string& originId, void CSVWorld::GenericCreator::toggleWidgets(bool active) { } + +void CSVWorld::GenericCreator::setScope (unsigned int scope) +{ + mScopes = scope; + int count = (mScopes & CSMWorld::Scope_Content) + (mScopes & CSMWorld::Scope_Project) + + (mScopes & CSMWorld::Scope_Session); + + // scope selector widget + if (count>1) + { + mScope = new QComboBox (this); + insertAtBeginning (mScope, false); + + if (mScopes & CSMWorld::Scope_Content) + addScope ("Content", CSMWorld::Scope_Content, + "Record will be stored in the currently edited content file."); + + if (mScopes & CSMWorld::Scope_Project) + addScope ("Project", CSMWorld::Scope_Project, + "Record will be stored in a local project file.

    " + "Record will be created in the reserved namespace \"project\".

    " + "Record is available when running OpenMW via OpenCS."); + + if (mScopes & CSMWorld::Scope_Session) + addScope ("Session", CSMWorld::Scope_Session, + "Record exists only for the duration of the current editing session.

    " + "Record will be created in the reserved namespace \"session\".

    " + "Record is not available when running OpenMW via OpenCS."); + + connect (mScope, SIGNAL (currentIndexChanged (int)), this, SLOT (scopeChanged (int))); + + mScopeLabel = new QLabel ("Scope", this); + insertAtBeginning (mScopeLabel, false); + + mScope->setCurrentIndex (0); + } + else + { + delete mScope; + mScope = 0; + + delete mScopeLabel; + mScopeLabel = 0; + } + + updateNamespace(); +} + +void CSVWorld::GenericCreator::scopeChanged (int index) +{ + update(); + updateNamespace(); +} \ No newline at end of file diff --git a/apps/opencs/view/world/genericcreator.hpp b/apps/opencs/view/world/genericcreator.hpp index 714853f98..8c8c34bd8 100644 --- a/apps/opencs/view/world/genericcreator.hpp +++ b/apps/opencs/view/world/genericcreator.hpp @@ -1,14 +1,18 @@ #ifndef CSV_WORLD_GENERICCREATOR_H #define CSV_WORLD_GENERICCREATOR_H +#include + +#include "../../model/world/universalid.hpp" + +#include "creator.hpp" + class QString; class QPushButton; class QLineEdit; class QHBoxLayout; - -#include "creator.hpp" - -#include "../../model/world/universalid.hpp" +class QComboBox; +class QLabel; namespace CSMWorld { @@ -17,6 +21,8 @@ namespace CSMWorld namespace CSVWorld { + class IdValidator; + class GenericCreator : public Creator { Q_OBJECT @@ -31,6 +37,10 @@ namespace CSVWorld bool mLocked; std::string mClonedId; CSMWorld::UniversalId::Type mClonedType; + unsigned int mScopes; + QComboBox *mScope; + QLabel *mScopeLabel; + IdValidator *mValidator; protected: bool mCloneMode; @@ -48,12 +58,29 @@ namespace CSVWorld virtual std::string getId() const; + /// Allow subclasses to add additional data to \a command. virtual void configureCreateCommand (CSMWorld::CreateCommand& command) const; + /// Allow subclasses to wrap the create command together with additional commands + /// into a macro. + virtual void pushCommand (std::auto_ptr command, + const std::string& id); + CSMWorld::Data& getData() const; + QUndoStack& getUndoStack(); + const CSMWorld::UniversalId& getCollectionId() const; + std::string getNamespace() const; + + private: + + void updateNamespace(); + + void addScope (const QString& name, CSMWorld::Scope scope, + const QString& tooltip); + public: GenericCreator (CSMWorld::Data& data, QUndoStack& undoStack, @@ -65,18 +92,22 @@ namespace CSVWorld virtual void toggleWidgets (bool active = true); - virtual void cloneMode(const std::string& originId, + virtual void cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type); virtual std::string getErrors() const; ///< Return formatted error descriptions for the current state of the creator. if an empty /// string is returned, there is no error. + virtual void setScope (unsigned int scope); + private slots: void textChanged (const QString& text); void create(); + + void scopeChanged (int index); }; } diff --git a/apps/opencs/view/world/idtypedelegate.cpp b/apps/opencs/view/world/idtypedelegate.cpp index 6b4d442f3..3b440ff71 100755 --- a/apps/opencs/view/world/idtypedelegate.cpp +++ b/apps/opencs/view/world/idtypedelegate.cpp @@ -3,9 +3,9 @@ #include "../../model/world/universalid.hpp" CSVWorld::IdTypeDelegate::IdTypeDelegate - (const ValueList &values, const IconList &icons, QUndoStack& undoStack, QObject *parent) - : DataDisplayDelegate (values, icons, undoStack, - "Display Format", "Referenceable ID Type Display", + (const ValueList &values, const IconList &icons, CSMDoc::Document& document, QObject *parent) + : DataDisplayDelegate (values, icons, document, + "records", "type-format", parent) {} @@ -20,8 +20,8 @@ CSVWorld::IdTypeDelegateFactory::IdTypeDelegateFactory() } } -CSVWorld::CommandDelegate *CSVWorld::IdTypeDelegateFactory::makeDelegate (QUndoStack& undoStack, - QObject *parent) const +CSVWorld::CommandDelegate *CSVWorld::IdTypeDelegateFactory::makeDelegate ( + CSMDoc::Document& document, QObject *parent) const { - return new IdTypeDelegate (mValues, mIcons, undoStack, parent); + return new IdTypeDelegate (mValues, mIcons, document, parent); } diff --git a/apps/opencs/view/world/idtypedelegate.hpp b/apps/opencs/view/world/idtypedelegate.hpp index bed81f2d5..e9a0af68c 100755 --- a/apps/opencs/view/world/idtypedelegate.hpp +++ b/apps/opencs/view/world/idtypedelegate.hpp @@ -11,7 +11,7 @@ namespace CSVWorld class IdTypeDelegate : public DataDisplayDelegate { public: - IdTypeDelegate (const ValueList &mValues, const IconList &icons, QUndoStack& undoStack, QObject *parent); + IdTypeDelegate (const ValueList &mValues, const IconList &icons, CSMDoc::Document& document, QObject *parent); }; class IdTypeDelegateFactory : public DataDisplayDelegateFactory @@ -20,7 +20,7 @@ namespace CSVWorld IdTypeDelegateFactory(); - virtual CommandDelegate *makeDelegate (QUndoStack& undoStack, QObject *parent) const; + virtual CommandDelegate *makeDelegate (CSMDoc::Document& document, QObject *parent) const; ///< The ownership of the returned CommandDelegate is transferred to the caller. }; } diff --git a/apps/opencs/view/world/idvalidator.cpp b/apps/opencs/view/world/idvalidator.cpp index 7c210daae..7caa20f9b 100644 --- a/apps/opencs/view/world/idvalidator.cpp +++ b/apps/opencs/view/world/idvalidator.cpp @@ -1,6 +1,8 @@ #include "idvalidator.hpp" +#include + bool CSVWorld::IdValidator::isValid (const QChar& c, bool first) const { if (c.isLetter() || c=='_') @@ -18,6 +20,8 @@ CSVWorld::IdValidator::IdValidator (bool relaxed, QObject *parent) QValidator::State CSVWorld::IdValidator::validate (QString& input, int& pos) const { + mError.clear(); + if (mRelaxed) { if (input.indexOf ('"')!=-1 || input.indexOf ("::")!=-1 || input.indexOf ("#")!=-1) @@ -25,12 +29,95 @@ QValidator::State CSVWorld::IdValidator::validate (QString& input, int& pos) con } else { + if (input.isEmpty()) + { + mError = "Missing ID"; + return QValidator::Intermediate; + } + bool first = true; + bool scope = false; + bool prevScope = false; + + QString::const_iterator iter = input.begin(); + + if (!mNamespace.empty()) + { + std::string namespace_ = input.left (mNamespace.size()).toUtf8().constData(); + + if (Misc::StringUtils::lowerCase (namespace_)!=mNamespace) + return QValidator::Invalid; // incorrect namespace + + iter += namespace_.size(); + first = false; + prevScope = true; + } + else + { + int index = input.indexOf (":"); + + if (index!=-1) + { + QString namespace_ = input.left (index); + + if (namespace_=="project" || namespace_=="session") + return QValidator::Invalid; // reserved namespace + } + } + + for (; iter!=input.end(); ++iter, first = false) + { + if (*iter==':') + { + if (first) + return QValidator::Invalid; // scope operator at the beginning - for (QString::const_iterator iter (input.begin()); iter!=input.end(); ++iter, first = false) - if (!isValid (*iter, first)) - return QValidator::Invalid; + if (scope) + { + scope = false; + prevScope = true; + } + else + { + if (prevScope) + return QValidator::Invalid; // sequence of two scope operators + + scope = true; + } + } + else if (scope) + return QValidator::Invalid; // incomplete scope operator + else + { + prevScope = false; + + if (!isValid (*iter, first)) + return QValidator::Invalid; + } + } + + if (scope) + { + mError = "ID ending with incomplete scope operator"; + return QValidator::Intermediate; + } + + if (prevScope) + { + mError = "ID ending with scope operator"; + return QValidator::Intermediate; + } } return QValidator::Acceptable; +} + +void CSVWorld::IdValidator::setNamespace (const std::string& namespace_) +{ + mNamespace = Misc::StringUtils::lowerCase (namespace_); +} + +std::string CSVWorld::IdValidator::getError() const +{ + return mError; } \ No newline at end of file diff --git a/apps/opencs/view/world/idvalidator.hpp b/apps/opencs/view/world/idvalidator.hpp index 8ca162440..a9df9580a 100644 --- a/apps/opencs/view/world/idvalidator.hpp +++ b/apps/opencs/view/world/idvalidator.hpp @@ -1,6 +1,8 @@ #ifndef CSV_WORLD_IDVALIDATOR_H #define CSV_WORLD_IDVALIDATOR_H +#include + #include namespace CSVWorld @@ -8,6 +10,8 @@ namespace CSVWorld class IdValidator : public QValidator { bool mRelaxed; + std::string mNamespace; + mutable std::string mError; private: @@ -20,6 +24,14 @@ namespace CSVWorld virtual State validate (QString& input, int& pos) const; + void setNamespace (const std::string& namespace_); + + /// Return a description of the error that resulted in the last call of validate + /// returning QValidator::Intermediate. If the last call to validate returned + /// a different value (or if there was no such call yet), an empty string is + /// returned. + std::string getError() const; + }; } diff --git a/apps/opencs/view/world/infocreator.cpp b/apps/opencs/view/world/infocreator.cpp index f09222930..1d914716b 100644 --- a/apps/opencs/view/world/infocreator.cpp +++ b/apps/opencs/view/world/infocreator.cpp @@ -53,6 +53,22 @@ CSVWorld::InfoCreator::InfoCreator (CSMWorld::Data& data, QUndoStack& undoStack, connect (mTopic, SIGNAL (textChanged (const QString&)), this, SLOT (topicChanged())); } +void CSVWorld::InfoCreator::cloneMode (const std::string& originId, + const CSMWorld::UniversalId::Type type) +{ + CSMWorld::IdTable& infoTable = + dynamic_cast (*getData().getTableModel (getCollectionId())); + + int topicColumn = infoTable.findColumnIndex ( + getCollectionId().getType()==CSMWorld::UniversalId::Type_TopicInfos ? + CSMWorld::Columns::ColumnId_Topic : CSMWorld::Columns::ColumnId_Journal); + + mTopic->setText ( + infoTable.data (infoTable.getModelIndex (originId, topicColumn)).toString()); + + GenericCreator::cloneMode (originId, type); +} + void CSVWorld::InfoCreator::reset() { mTopic->setText (""); diff --git a/apps/opencs/view/world/infocreator.hpp b/apps/opencs/view/world/infocreator.hpp index e9cb7e596..2296a8297 100644 --- a/apps/opencs/view/world/infocreator.hpp +++ b/apps/opencs/view/world/infocreator.hpp @@ -27,6 +27,9 @@ namespace CSVWorld InfoCreator (CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id); + virtual void cloneMode (const std::string& originId, + const CSMWorld::UniversalId::Type type); + virtual void reset(); virtual std::string getErrors() const; diff --git a/apps/opencs/view/world/physicssystem.cpp b/apps/opencs/view/world/physicssystem.cpp new file mode 100644 index 000000000..2cbe17dcf --- /dev/null +++ b/apps/opencs/view/world/physicssystem.cpp @@ -0,0 +1,324 @@ +#include "physicssystem.hpp" + +#include + +#include +#include +#include + +#include +#include +#include "../../model/settings/usersettings.hpp" +#include "../render/elements.hpp" + +namespace CSVWorld +{ + PhysicsSystem::PhysicsSystem() + { + // Create physics. shapeLoader is deleted by the physic engine + NifBullet::ManualBulletShapeLoader* shapeLoader = new NifBullet::ManualBulletShapeLoader(true); + mEngine = new OEngine::Physic::PhysicEngine(shapeLoader); + } + + PhysicsSystem::~PhysicsSystem() + { + delete mEngine; + } + + // looks up the scene manager based on the scene node name (inefficient) + // NOTE: referenceId is assumed to be unique per document + // NOTE: searching is done here rather than after rayTest, hence slower to load but + // faster to find (guessing, not verified w/ perf test) + void PhysicsSystem::addObject(const std::string &mesh, + const std::string &sceneNodeName, const std::string &referenceId, float scale, + const Ogre::Vector3 &position, const Ogre::Quaternion &rotation, bool placeable) + { + Ogre::SceneManager *sceneManager = findSceneManager(sceneNodeName); + if(sceneManager) + { + // update maps (NOTE: sometimes replaced) + mSceneNodeToRefId[sceneNodeName] = referenceId; + mSceneNodeToMesh[sceneNodeName] = mesh; + mRefIdToSceneNode[referenceId][sceneManager] = sceneNodeName; + } + else + { + std::cerr << "Attempt to add an object without a corresponding SceneManager: " + + referenceId + " : " + sceneNodeName << std::endl; + return; + } + + // update physics, only one physics model per referenceId + if(mEngine->getRigidBody(referenceId, true) == NULL) + { + mEngine->createAndAdjustRigidBody(mesh, + referenceId, scale, position, rotation, + 0, // scaledBoxTranslation + 0, // boxRotation + true, // raycasting + placeable); + } + } + + // normal delete (e.g closing a scene subview or ~Object()) + // the scene node is destroyed so the mappings should be removed + // + // TODO: should think about using some kind of reference counting within RigidBody + void PhysicsSystem::removeObject(const std::string &sceneNodeName) + { + std::string referenceId = mSceneNodeToRefId[sceneNodeName]; + + if(referenceId != "") + { + mSceneNodeToRefId.erase(sceneNodeName); + mSceneNodeToMesh.erase(sceneNodeName); + + // find which SceneManager has this object + Ogre::SceneManager *sceneManager = findSceneManager(sceneNodeName); + if(!sceneManager) + { + std::cerr << "Attempt to remove an object without a corresponding SceneManager: " + + sceneNodeName << std::endl; + return; + } + + // illustration: erase the object "K" from the object map + // + // RidigBody SubView Ogre + // --------------- -------------- ------------- + // ReferenceId "A" (SceneManager X SceneNode "J") + // (SceneManager Y SceneNode "K") <--- erase + // (SceneManager Z SceneNode "L") + // + // ReferenceId "B" (SceneManager X SceneNode "M") + // (SceneManager Y SceneNode "N") <--- notice not deleted + // (SceneManager Z SceneNode "O") + std::map >::iterator itRef = + mRefIdToSceneNode.begin(); + for(; itRef != mRefIdToSceneNode.end(); ++itRef) + { + if((*itRef).second.find(sceneManager) != (*itRef).second.end()) + { + (*itRef).second.erase(sceneManager); + break; + } + } + + // check whether the physics model should be deleted + if(mRefIdToSceneNode.find(referenceId) == mRefIdToSceneNode.end()) + { + mEngine->removeRigidBody(referenceId); + mEngine->deleteRigidBody(referenceId); + } + } + } + + // Object::clear() is called when reference data is changed. It clears all + // contents of the SceneNode and removes the physics object + // + // A new physics object will be created and assigned to this sceneNodeName by + // Object::update() + void PhysicsSystem::removePhysicsObject(const std::string &sceneNodeName) + { + std::string referenceId = mSceneNodeToRefId[sceneNodeName]; + + if(referenceId != "") + { + mEngine->removeRigidBody(referenceId); + mEngine->deleteRigidBody(referenceId); + } + } + + void PhysicsSystem::replaceObject(const std::string &sceneNodeName, float scale, + const Ogre::Vector3 &position, const Ogre::Quaternion &rotation, bool placeable) + { + std::string referenceId = mSceneNodeToRefId[sceneNodeName]; + std::string mesh = mSceneNodeToMesh[sceneNodeName]; + + if(referenceId != "") + { + // delete the physics object + mEngine->removeRigidBody(referenceId); + mEngine->deleteRigidBody(referenceId); + + // create a new physics object + mEngine->createAndAdjustRigidBody(mesh, referenceId, scale, position, rotation, + 0, 0, true, placeable); + + // update other scene managers if they have the referenceId + // FIXME: rotation or scale not updated + moveSceneNodeImpl(sceneNodeName, referenceId, position); + } + } + + // FIXME: adjustRigidBody() seems to lose objects, work around by deleting and recreating objects + void PhysicsSystem::moveObject(const std::string &sceneNodeName, + const Ogre::Vector3 &position, const Ogre::Quaternion &rotation) + { + mEngine->adjustRigidBody(mEngine->getRigidBody(sceneNodeName, true /*raycasting*/), + position, rotation); + } + + void PhysicsSystem::moveSceneNodeImpl(const std::string sceneNodeName, + const std::string referenceId, const Ogre::Vector3 &position) + { + std::map::const_iterator iter = mSceneWidgets.begin(); + for(; iter != mSceneWidgets.end(); ++iter) + { + std::string name = refIdToSceneNode(referenceId, (*iter).first); + if(name != sceneNodeName && (*iter).first->hasSceneNode(name)) + { + (*iter).first->getSceneNode(name)->setPosition(position); + } + } + } + + void PhysicsSystem::moveSceneNodes(const std::string sceneNodeName, const Ogre::Vector3 &position) + { + moveSceneNodeImpl(sceneNodeName, sceneNodeToRefId(sceneNodeName), position); + } + + void PhysicsSystem::addHeightField(Ogre::SceneManager *sceneManager, + float* heights, int x, int y, float yoffset, float triSize, float sqrtVerts) + { + std::string name = "HeightField_" + + QString::number(x).toStdString() + "_" + QString::number(y).toStdString(); + + if(mTerrain.find(name) == mTerrain.end()) + mEngine->addHeightField(heights, x, y, yoffset, triSize, sqrtVerts); + + mTerrain.insert(std::pair(name, sceneManager)); + } + + void PhysicsSystem::removeHeightField(Ogre::SceneManager *sceneManager, int x, int y) + { + std::string name = "HeightField_" + + QString::number(x).toStdString() + "_" + QString::number(y).toStdString(); + + if(mTerrain.count(name) == 1) + mEngine->removeHeightField(x, y); + + std::multimap::iterator iter = mTerrain.begin(); + for(; iter != mTerrain.end(); ++iter) + { + if((*iter).second == sceneManager) + { + mTerrain.erase(iter); + break; + } + } + } + + // sceneMgr: to lookup the scene node name from the object's referenceId + // camera: primarily used to get the visibility mask for the viewport + // + // returns the found object's scene node name and its position in the world space + // + // WARNING: far clip distance is a global setting, if it changes in future + // this method will need to be updated + std::pair PhysicsSystem::castRay(float mouseX, + float mouseY, Ogre::SceneManager *sceneMgr, Ogre::Camera *camera) + { + // NOTE: there could be more than one camera for the scene manager + // TODO: check whether camera belongs to sceneMgr + if(!sceneMgr || !camera || !camera->getViewport()) + return std::make_pair("", Ogre::Vector3(0,0,0)); // FIXME: this should be an exception + + // using a really small value seems to mess up with the projections + float nearClipDistance = camera->getNearClipDistance(); // save existing + camera->setNearClipDistance(10.0f); // arbitrary number + Ogre::Ray ray = camera->getCameraToViewportRay(mouseX, mouseY); + camera->setNearClipDistance(nearClipDistance); // restore + + Ogre::Vector3 from = ray.getOrigin(); + CSMSettings::UserSettings &userSettings = CSMSettings::UserSettings::instance(); + float farClipDist = userSettings.setting("Scene/far clip distance", QString("300000")).toFloat(); + Ogre::Vector3 to = ray.getPoint(farClipDist); + + btVector3 _from, _to; + _from = btVector3(from.x, from.y, from.z); + _to = btVector3(to.x, to.y, to.z); + + uint32_t visibilityMask = camera->getViewport()->getVisibilityMask(); + bool ignoreHeightMap = !(visibilityMask & (uint32_t)CSVRender::Element_Terrain); + bool ignoreObjects = !(visibilityMask & (uint32_t)CSVRender::Element_Reference); + + Ogre::Vector3 norm; // not used + std::pair result = + mEngine->rayTest(_from, _to, !ignoreObjects, ignoreHeightMap, &norm); + + // result.first is the object's referenceId + if(result.first == "") + return std::make_pair("", Ogre::Vector3(0,0,0)); + else + { + std::string name = refIdToSceneNode(result.first, sceneMgr); + if(name == "") + name = result.first; + else + name = refIdToSceneNode(result.first, sceneMgr); + + return std::make_pair(name, ray.getPoint(farClipDist*result.second)); + } + } + + std::string PhysicsSystem::refIdToSceneNode(std::string referenceId, Ogre::SceneManager *sceneMgr) + { + return mRefIdToSceneNode[referenceId][sceneMgr]; + } + + std::string PhysicsSystem::sceneNodeToRefId(std::string sceneNodeName) + { + return mSceneNodeToRefId[sceneNodeName]; + } + + void PhysicsSystem::addSceneManager(Ogre::SceneManager *sceneMgr, CSVRender::SceneWidget *sceneWidget) + { + mSceneWidgets[sceneMgr] = sceneWidget; + + mEngine->createDebugDraw(sceneMgr); + } + + std::map PhysicsSystem::sceneWidgets() + { + return mSceneWidgets; + } + + void PhysicsSystem::removeSceneManager(Ogre::SceneManager *sceneMgr) + { + mEngine->removeDebugDraw(sceneMgr); + + mSceneWidgets.erase(sceneMgr); + } + + Ogre::SceneManager *PhysicsSystem::findSceneManager(std::string sceneNodeName) + { + std::map::const_iterator iter = mSceneWidgets.begin(); + for(; iter != mSceneWidgets.end(); ++iter) + { + if((*iter).first->hasSceneNode(sceneNodeName)) + { + return (*iter).first; + } + } + + return NULL; + } + + void PhysicsSystem::toggleDebugRendering(Ogre::SceneManager *sceneMgr) + { + // FIXME: should check if sceneMgr is in the list + if(!sceneMgr) + return; + + CSMSettings::UserSettings &userSettings = CSMSettings::UserSettings::instance(); + if(!(userSettings.setting("debug/mouse-picking", QString("false")) == "true" ? true : false)) + { + std::cerr << "Turn on mouse-picking debug option to see collision shapes." << std::endl; + return; + } + + mEngine->toggleDebugRendering(sceneMgr); + mEngine->stepDebug(sceneMgr); + } +} diff --git a/apps/opencs/view/world/physicssystem.hpp b/apps/opencs/view/world/physicssystem.hpp new file mode 100644 index 000000000..0036bf769 --- /dev/null +++ b/apps/opencs/view/world/physicssystem.hpp @@ -0,0 +1,94 @@ +#ifndef CSV_WORLD_PHYSICSSYSTEM_H +#define CSV_WORLD_PHYSICSSYSTEM_H + +#include +#include + +namespace Ogre +{ + class Vector3; + class Quaternion; + class SceneManager; + class Camera; +} + +namespace OEngine +{ + namespace Physic + { + class PhysicEngine; + } +} + +namespace CSVRender +{ + class SceneWidget; +} + +namespace CSVWorld +{ + class PhysicsSystem + { + std::map mSceneNodeToRefId; + std::map > mRefIdToSceneNode; + std::map mSceneNodeToMesh; + std::map mSceneWidgets; + OEngine::Physic::PhysicEngine* mEngine; + std::multimap mTerrain; + + public: + + PhysicsSystem(); + ~PhysicsSystem(); + + void addSceneManager(Ogre::SceneManager *sceneMgr, CSVRender::SceneWidget * scene); + + void removeSceneManager(Ogre::SceneManager *sceneMgr); + + void addObject(const std::string &mesh, + const std::string &sceneNodeName, const std::string &referenceId, float scale, + const Ogre::Vector3 &position, const Ogre::Quaternion &rotation, + bool placeable=false); + + void removeObject(const std::string &sceneNodeName); + void removePhysicsObject(const std::string &sceneNodeName); + + void replaceObject(const std::string &sceneNodeName, + float scale, const Ogre::Vector3 &position, + const Ogre::Quaternion &rotation, bool placeable=false); + + void moveObject(const std::string &sceneNodeName, + const Ogre::Vector3 &position, const Ogre::Quaternion &rotation); + + void moveSceneNodes(const std::string sceneNodeName, const Ogre::Vector3 &position); + + void addHeightField(Ogre::SceneManager *sceneManager, + float* heights, int x, int y, float yoffset, float triSize, float sqrtVerts); + + void removeHeightField(Ogre::SceneManager *sceneManager, int x, int y); + + void toggleDebugRendering(Ogre::SceneManager *sceneMgr); + + // return the object's SceneNode name and position for the given SceneManager + std::pair castRay(float mouseX, + float mouseY, Ogre::SceneManager *sceneMgr, Ogre::Camera *camera); + + std::string sceneNodeToRefId(std::string sceneNodeName); + + // for multi-scene manager per physics engine + std::map sceneWidgets(); + + private: + + void moveSceneNodeImpl(const std::string sceneNodeName, + const std::string referenceId, const Ogre::Vector3 &position); + + void updateSelectionHighlight(std::string sceneNode, const Ogre::Vector3 &position); + + std::string refIdToSceneNode(std::string referenceId, Ogre::SceneManager *sceneMgr); + + Ogre::SceneManager *findSceneManager(std::string sceneNodeName); + }; +} + +#endif // CSV_WORLD_PHYSICSSYSTEM_H diff --git a/apps/opencs/view/world/previewsubview.cpp b/apps/opencs/view/world/previewsubview.cpp index 1e106c69f..1ae466f42 100644 --- a/apps/opencs/view/world/previewsubview.cpp +++ b/apps/opencs/view/world/previewsubview.cpp @@ -9,7 +9,7 @@ #include "../widget/scenetoolmode.hpp" CSVWorld::PreviewSubView::PreviewSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document) -: SubView (id) +: SubView (id), mTitle (id.toString().c_str()) { QHBoxLayout *layout = new QHBoxLayout; @@ -52,15 +52,19 @@ CSVWorld::PreviewSubView::PreviewSubView (const CSMWorld::UniversalId& id, CSMDo void CSVWorld::PreviewSubView::setEditLock (bool locked) {} -void CSVWorld::PreviewSubView::closeRequest() +std::string CSVWorld::PreviewSubView::getTitle() const { - deleteLater(); + return mTitle; } void CSVWorld::PreviewSubView::referenceableIdChanged (const std::string& id) { if (id.empty()) - setWindowTitle ("Preview: Reference to "); + mTitle = "Preview: Reference to "; else - setWindowTitle (("Preview: Reference to " + id).c_str()); + mTitle = "Preview: Reference to " + id; + + setWindowTitle (QString::fromUtf8 (mTitle.c_str())); + + emit updateTitle(); } \ No newline at end of file diff --git a/apps/opencs/view/world/previewsubview.hpp b/apps/opencs/view/world/previewsubview.hpp index 4ca25c3cb..a28be5c36 100644 --- a/apps/opencs/view/world/previewsubview.hpp +++ b/apps/opencs/view/world/previewsubview.hpp @@ -20,6 +20,7 @@ namespace CSVWorld Q_OBJECT CSVRender::PreviewWidget *mScene; + std::string mTitle; public: @@ -27,9 +28,9 @@ namespace CSVWorld virtual void setEditLock (bool locked); - private slots: + virtual std::string getTitle() const; - void closeRequest(); + private slots: void referenceableIdChanged (const std::string& id); }; diff --git a/apps/opencs/view/world/recordstatusdelegate.cpp b/apps/opencs/view/world/recordstatusdelegate.cpp index 4fe7031ce..708a78015 100644 --- a/apps/opencs/view/world/recordstatusdelegate.cpp +++ b/apps/opencs/view/world/recordstatusdelegate.cpp @@ -9,16 +9,16 @@ CSVWorld::RecordStatusDelegate::RecordStatusDelegate(const ValueList& values, const IconList & icons, - QUndoStack &undoStack, QObject *parent) - : DataDisplayDelegate (values, icons, undoStack, - "Display Format", "Record Status Display", + CSMDoc::Document& document, QObject *parent) + : DataDisplayDelegate (values, icons, document, + "records", "status-format", parent) {} -CSVWorld::CommandDelegate *CSVWorld::RecordStatusDelegateFactory::makeDelegate (QUndoStack& undoStack, - QObject *parent) const +CSVWorld::CommandDelegate *CSVWorld::RecordStatusDelegateFactory::makeDelegate ( + CSMDoc::Document& document, QObject *parent) const { - return new RecordStatusDelegate (mValues, mIcons, undoStack, parent); + return new RecordStatusDelegate (mValues, mIcons, document, parent); } CSVWorld::RecordStatusDelegateFactory::RecordStatusDelegateFactory() diff --git a/apps/opencs/view/world/recordstatusdelegate.hpp b/apps/opencs/view/world/recordstatusdelegate.hpp index 1b42223af..fbdaed538 100644 --- a/apps/opencs/view/world/recordstatusdelegate.hpp +++ b/apps/opencs/view/world/recordstatusdelegate.hpp @@ -19,7 +19,7 @@ namespace CSVWorld explicit RecordStatusDelegate(const ValueList& values, const IconList& icons, - QUndoStack& undoStack, QObject *parent = 0); + CSMDoc::Document& document, QObject *parent = 0); }; class RecordStatusDelegateFactory : public DataDisplayDelegateFactory @@ -28,7 +28,7 @@ namespace CSVWorld RecordStatusDelegateFactory(); - virtual CommandDelegate *makeDelegate (QUndoStack& undoStack, QObject *parent) const; + virtual CommandDelegate *makeDelegate (CSMDoc::Document& document, QObject *parent) const; ///< The ownership of the returned CommandDelegate is transferred to the caller. }; diff --git a/apps/opencs/view/world/referenceablecreator.cpp b/apps/opencs/view/world/referenceablecreator.cpp index 7a5fca853..e8055ed31 100644 --- a/apps/opencs/view/world/referenceablecreator.cpp +++ b/apps/opencs/view/world/referenceablecreator.cpp @@ -42,6 +42,13 @@ void CSVWorld::ReferenceableCreator::reset() GenericCreator::reset(); } +void CSVWorld::ReferenceableCreator::cloneMode (const std::string& originId, + const CSMWorld::UniversalId::Type type) +{ + GenericCreator::cloneMode (originId, type); + mType->setCurrentIndex (mType->findData (static_cast (type))); +} + void CSVWorld::ReferenceableCreator::toggleWidgets(bool active) { CSVWorld::GenericCreator::toggleWidgets(active); diff --git a/apps/opencs/view/world/referenceablecreator.hpp b/apps/opencs/view/world/referenceablecreator.hpp index 88545575e..14ad24b29 100644 --- a/apps/opencs/view/world/referenceablecreator.hpp +++ b/apps/opencs/view/world/referenceablecreator.hpp @@ -23,6 +23,10 @@ namespace CSVWorld const CSMWorld::UniversalId& id); virtual void reset(); + + virtual void cloneMode (const std::string& originId, + const CSMWorld::UniversalId::Type type); + virtual void toggleWidgets(bool active = true); }; diff --git a/apps/opencs/view/world/referencecreator.cpp b/apps/opencs/view/world/referencecreator.cpp index 6b8e02da0..1e3cc00d7 100644 --- a/apps/opencs/view/world/referencecreator.cpp +++ b/apps/opencs/view/world/referencecreator.cpp @@ -16,11 +16,58 @@ std::string CSVWorld::ReferenceCreator::getId() const void CSVWorld::ReferenceCreator::configureCreateCommand (CSMWorld::CreateCommand& command) const { - int index = + // Set cellID + int cellIdColumn = dynamic_cast (*getData().getTableModel (getCollectionId())). findColumnIndex (CSMWorld::Columns::ColumnId_Cell); - command.addValue (index, mCell->text()); + command.addValue (cellIdColumn, mCell->text()); + + // Set RefNum + int refNumColumn = dynamic_cast ( + *getData().getTableModel (CSMWorld::UniversalId::Type_References)). + findColumnIndex (CSMWorld::Columns::ColumnId_RefNum); + + command.addValue (refNumColumn, getRefNumCount()); +} + +void CSVWorld::ReferenceCreator::pushCommand (std::auto_ptr command, + const std::string& id) +{ + // get the old count + std::string cellId = mCell->text().toUtf8().constData(); + + CSMWorld::IdTable& cellTable = dynamic_cast ( + *getData().getTableModel (CSMWorld::UniversalId::Type_Cells)); + + int countColumn = cellTable.findColumnIndex (CSMWorld::Columns::ColumnId_RefNumCounter); + + QModelIndex countIndex = cellTable.getModelIndex (cellId, countColumn); + + int count = cellTable.data (countIndex).toInt(); + + // command for incrementing counter + std::auto_ptr increment (new CSMWorld::ModifyCommand + (cellTable, countIndex, count+1)); + + getUndoStack().beginMacro (command->text()); + GenericCreator::pushCommand (command, id); + getUndoStack().push (increment.release()); + getUndoStack().endMacro(); +} + +int CSVWorld::ReferenceCreator::getRefNumCount() const +{ + std::string cellId = mCell->text().toUtf8().constData(); + + CSMWorld::IdTable& cellTable = dynamic_cast ( + *getData().getTableModel (CSMWorld::UniversalId::Type_Cells)); + + int countColumn = cellTable.findColumnIndex (CSMWorld::Columns::ColumnId_RefNumCounter); + + QModelIndex countIndex = cellTable.getModelIndex (cellId, countColumn); + + return cellTable.data (countIndex).toInt(); } CSVWorld::ReferenceCreator::ReferenceCreator (CSMWorld::Data& data, QUndoStack& undoStack, @@ -47,12 +94,9 @@ void CSVWorld::ReferenceCreator::reset() std::string CSVWorld::ReferenceCreator::getErrors() const { - std::string errors = GenericCreator::getErrors(); - - if (mCloneMode) - { - return errors; - } + // We are ignoring errors coming from GenericCreator here, because the ID of the new + // record is internal and requires neither user input nor verification. + std::string errors; std::string cell = mCell->text().toUtf8().constData(); @@ -79,15 +123,17 @@ void CSVWorld::ReferenceCreator::cellChanged() update(); } -void CSVWorld::ReferenceCreator::toggleWidgets(bool active) -{ - CSVWorld::GenericCreator::toggleWidgets(active); - mCell->setEnabled(active); -} - void CSVWorld::ReferenceCreator::cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) { + CSMWorld::IdTable& referenceTable = dynamic_cast ( + *getData().getTableModel (CSMWorld::UniversalId::Type_References)); + + int cellIdColumn = referenceTable.findColumnIndex (CSMWorld::Columns::ColumnId_Cell); + + mCell->setText ( + referenceTable.data (referenceTable.getModelIndex (originId, cellIdColumn)).toString()); + CSVWorld::GenericCreator::cloneMode(originId, type); cellChanged(); //otherwise ok button will remain disabled } diff --git a/apps/opencs/view/world/referencecreator.hpp b/apps/opencs/view/world/referencecreator.hpp index 12fb12dd9..002a62d87 100644 --- a/apps/opencs/view/world/referencecreator.hpp +++ b/apps/opencs/view/world/referencecreator.hpp @@ -20,16 +20,20 @@ namespace CSVWorld virtual void configureCreateCommand (CSMWorld::CreateCommand& command) const; + virtual void pushCommand (std::auto_ptr command, + const std::string& id); + + int getRefNumCount() const; + public: ReferenceCreator (CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id); - virtual void cloneMode(const std::string& originId, + virtual void cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type); virtual void reset(); - virtual void toggleWidgets(bool active = true); virtual std::string getErrors() const; ///< Return formatted error descriptions for the current state of the creator. if an empty diff --git a/apps/opencs/view/world/scenesubview.cpp b/apps/opencs/view/world/scenesubview.cpp index dc1525b05..3fdf2f6e5 100644 --- a/apps/opencs/view/world/scenesubview.cpp +++ b/apps/opencs/view/world/scenesubview.cpp @@ -19,6 +19,9 @@ #include "../widget/scenetoolbar.hpp" #include "../widget/scenetoolmode.hpp" +#include "../widget/scenetooltoggle.hpp" +#include "../widget/scenetooltoggle2.hpp" +#include "../widget/scenetoolrun.hpp" #include "tablebottombox.hpp" #include "creator.hpp" @@ -36,7 +39,7 @@ CSVWorld::SceneSubView::SceneSubView (const CSMWorld::UniversalId& id, CSMDoc::D mLayout->setContentsMargins (QMargins (0, 0, 0, 0)); - CSVRender::WorldspaceWidget* wordspaceWidget = NULL; + CSVRender::WorldspaceWidget* worldspaceWidget = NULL; widgetType whatWidget; if (id.getId()=="sys::default") @@ -45,7 +48,7 @@ CSVWorld::SceneSubView::SceneSubView (const CSMWorld::UniversalId& id, CSMDoc::D CSVRender::PagedWorldspaceWidget *newWidget = new CSVRender::PagedWorldspaceWidget (this, document); - wordspaceWidget = newWidget; + worldspaceWidget = newWidget; makeConnections(newWidget); } @@ -55,12 +58,12 @@ CSVWorld::SceneSubView::SceneSubView (const CSMWorld::UniversalId& id, CSMDoc::D CSVRender::UnpagedWorldspaceWidget *newWidget = new CSVRender::UnpagedWorldspaceWidget (id.getId(), document, this); - wordspaceWidget = newWidget; + worldspaceWidget = newWidget; makeConnections(newWidget); } - replaceToolbarAndWorldspace(wordspaceWidget, makeToolbar(wordspaceWidget, whatWidget)); + replaceToolbarAndWorldspace(worldspaceWidget, makeToolbar(worldspaceWidget, whatWidget)); layout->insertLayout (0, mLayout, 1); @@ -107,31 +110,36 @@ CSVWidget::SceneToolbar* CSVWorld::SceneSubView::makeToolbar (CSVRender::Worldsp CSVWidget::SceneToolMode *lightingTool = widget->makeLightingSelector (toolbar); toolbar->addTool (lightingTool); -/* Add buttons specific to the type. For now no need for it. - * - switch (type) - { - case widget_Paged: - break; + CSVWidget::SceneToolToggle2 *sceneVisibilityTool = + widget->makeSceneVisibilitySelector (toolbar); + toolbar->addTool (sceneVisibilityTool); - case widget_Unpaged: - break; + if (type==widget_Paged) + { + CSVWidget::SceneToolToggle *controlVisibilityTool = + dynamic_cast (*widget). + makeControlVisibilitySelector (toolbar); + toolbar->addTool (controlVisibilityTool); } -*/ + + CSVWidget::SceneToolRun *runTool = widget->makeRunTool (toolbar); + toolbar->addTool (runTool); + + CSVWidget::SceneToolMode *editModeTool = widget->makeEditModeSelector (toolbar); + toolbar->addTool (editModeTool); + return toolbar; } void CSVWorld::SceneSubView::setEditLock (bool locked) { - } void CSVWorld::SceneSubView::updateEditorSetting(const QString &settingName, const QString &settingValue) { - } void CSVWorld::SceneSubView::setStatusBar (bool show) @@ -144,9 +152,9 @@ void CSVWorld::SceneSubView::useHint (const std::string& hint) mScene->useViewHint (hint); } -void CSVWorld::SceneSubView::closeRequest() +std::string CSVWorld::SceneSubView::getTitle() const { - deleteLater(); + return mTitle; } void CSVWorld::SceneSubView::cellSelectionChanged (const CSMWorld::UniversalId& id) @@ -155,10 +163,11 @@ void CSVWorld::SceneSubView::cellSelectionChanged (const CSMWorld::UniversalId& std::ostringstream stream; stream << "Scene: " << getUniversalId().getId(); - setWindowTitle (QString::fromUtf8 (stream.str().c_str())); + mTitle = stream.str(); + setWindowTitle (QString::fromUtf8 (mTitle.c_str())); + emit updateTitle(); } - void CSVWorld::SceneSubView::cellSelectionChanged (const CSMWorld::CellSelection& selection) { setUniversalId(CSMWorld::UniversalId(CSMWorld::UniversalId::Type_Scene, "sys::default")); @@ -183,7 +192,9 @@ void CSVWorld::SceneSubView::cellSelectionChanged (const CSMWorld::CellSelection stream << "cell around it)"; } - setWindowTitle (QString::fromUtf8 (stream.str().c_str())); + mTitle = stream.str(); + setWindowTitle (QString::fromUtf8 (mTitle.c_str())); + emit updateTitle(); } void CSVWorld::SceneSubView::handleDrop (const std::vector< CSMWorld::UniversalId >& data) @@ -192,10 +203,12 @@ void CSVWorld::SceneSubView::handleDrop (const std::vector< CSMWorld::UniversalI CSVRender::UnpagedWorldspaceWidget* unPagedNewWidget = NULL; CSVWidget::SceneToolbar* toolbar = NULL; - switch (mScene->getDropRequirements(CSVRender::WorldspaceWidget::getDropType(data))) + CSVRender::WorldspaceWidget::DropType type = CSVRender::WorldspaceWidget::getDropType (data); + + switch (mScene->getDropRequirements (type)) { case CSVRender::WorldspaceWidget::canHandle: - mScene->handleDrop(data); + mScene->handleDrop (data, type); break; case CSVRender::WorldspaceWidget::needPaged: @@ -203,7 +216,7 @@ void CSVWorld::SceneSubView::handleDrop (const std::vector< CSMWorld::UniversalI toolbar = makeToolbar(pagedNewWidget, widget_Paged); makeConnections(pagedNewWidget); replaceToolbarAndWorldspace(pagedNewWidget, toolbar); - mScene->handleDrop(data); + mScene->handleDrop (data, type); break; case CSVRender::WorldspaceWidget::needUnpaged: @@ -239,6 +252,8 @@ void CSVWorld::SceneSubView::replaceToolbarAndWorldspace (CSVRender::WorldspaceW mToolbar = toolbar; connect (mScene, SIGNAL (focusToolbarRequest()), mToolbar, SLOT (setFocus())); + connect (this, SIGNAL (updateSceneUserSetting(const QString &, const QStringList &)), + mScene, SLOT (updateUserSetting(const QString &, const QStringList &))); connect (mToolbar, SIGNAL (focusSceneRequest()), mScene, SLOT (setFocus())); mLayout->addWidget (mToolbar, 0); @@ -246,4 +261,9 @@ void CSVWorld::SceneSubView::replaceToolbarAndWorldspace (CSVRender::WorldspaceW mScene->selectDefaultNavigationMode(); setFocusProxy (mScene); -} \ No newline at end of file +} + +void CSVWorld::SceneSubView::updateUserSetting (const QString &key, const QStringList &list) +{ + emit updateSceneUserSetting(key, list); +} diff --git a/apps/opencs/view/world/scenesubview.hpp b/apps/opencs/view/world/scenesubview.hpp index b9dcdd6a3..fc45347d0 100644 --- a/apps/opencs/view/world/scenesubview.hpp +++ b/apps/opencs/view/world/scenesubview.hpp @@ -44,6 +44,7 @@ namespace CSVWorld QHBoxLayout* mLayout; CSMDoc::Document& mDocument; CSVWidget::SceneToolbar* mToolbar; + std::string mTitle; public: @@ -57,6 +58,8 @@ namespace CSVWorld virtual void useHint (const std::string& hint); + virtual std::string getTitle() const; + private: void makeConnections(CSVRender::PagedWorldspaceWidget* widget); @@ -75,13 +78,19 @@ namespace CSVWorld private slots: - void closeRequest(); - void cellSelectionChanged (const CSMWorld::CellSelection& selection); void cellSelectionChanged (const CSMWorld::UniversalId& id); void handleDrop(const std::vector& data); + + public slots: + + void updateUserSetting (const QString &, const QStringList &); + + signals: + + void updateSceneUserSetting (const QString &, const QStringList &); }; } diff --git a/apps/opencs/view/world/scriptedit.cpp b/apps/opencs/view/world/scriptedit.cpp index 23bc76000..c2d94ab5d 100644 --- a/apps/opencs/view/world/scriptedit.cpp +++ b/apps/opencs/view/world/scriptedit.cpp @@ -6,14 +6,35 @@ #include #include +#include "../../model/doc/document.hpp" + #include "../../model/world/universalid.hpp" #include "../../model/world/tablemimedata.hpp" -CSVWorld::ScriptEdit::ScriptEdit (QWidget* parent, const CSMDoc::Document& document) : - QTextEdit (parent), + +CSVWorld::ScriptEdit::ChangeLock::ChangeLock (ScriptEdit& edit) : mEdit (edit) +{ + ++mEdit.mChangeLocked; +} + +CSVWorld::ScriptEdit::ChangeLock::~ChangeLock() +{ + --mEdit.mChangeLocked; +} + + +CSVWorld::ScriptEdit::ScriptEdit (const CSMDoc::Document& document, ScriptHighlighter::Mode mode, + QWidget* parent) + : QPlainTextEdit (parent), mDocument (document), - mWhiteListQoutes("^[a-z|_]{1}[a-z|0-9|_]{0,}$", Qt::CaseInsensitive) + mWhiteListQoutes("^[a-z|_]{1}[a-z|0-9|_]{0,}$", Qt::CaseInsensitive), + mChangeLocked (0) { +// setAcceptRichText (false); + setLineWrapMode (QPlainTextEdit::NoWrap); + setTabStopWidth (4); + setUndoRedoEnabled (false); // we use OpenCS-wide undo/redo instead + mAllowedTypes < (event->mimeData()); if (!mime) - QTextEdit::dragEnterEvent(event); + QPlainTextEdit::dragEnterEvent(event); else { setTextCursor (cursorForPosition (event->pos())); @@ -59,7 +95,7 @@ void CSVWorld::ScriptEdit::dragMoveEvent (QDragMoveEvent* event) { const CSMWorld::TableMimeData* mime = dynamic_cast (event->mimeData()); if (!mime) - QTextEdit::dragMoveEvent(event); + QPlainTextEdit::dragMoveEvent(event); else { setTextCursor (cursorForPosition (event->pos())); @@ -72,7 +108,7 @@ void CSVWorld::ScriptEdit::dropEvent (QDropEvent* event) const CSMWorld::TableMimeData* mime = dynamic_cast (event->mimeData()); if (!mime) // May happen when non-records (e.g. plain text) are dragged and dropped { - QTextEdit::dropEvent(event); + QPlainTextEdit::dropEvent(event); return; } @@ -103,3 +139,21 @@ bool CSVWorld::ScriptEdit::stringNeedsQuote (const std::string& id) const //I'm not quite sure when do we need to put quotes. To be safe we will use quotes for anything other than… return !(string.contains(mWhiteListQoutes)); } + +void CSVWorld::ScriptEdit::idListChanged() +{ + mHighlighter->invalidateIds(); + + if (!mUpdateTimer.isActive()) + mUpdateTimer.start (0); +} + +void CSVWorld::ScriptEdit::updateHighlighting() +{ + if (isChangeLocked()) + return; + + ChangeLock lock (*this); + + mHighlighter->rehighlight(); +} \ No newline at end of file diff --git a/apps/opencs/view/world/scriptedit.hpp b/apps/opencs/view/world/scriptedit.hpp index b4627c2fe..c67385816 100644 --- a/apps/opencs/view/world/scriptedit.hpp +++ b/apps/opencs/view/world/scriptedit.hpp @@ -1,11 +1,14 @@ #ifndef SCRIPTEDIT_H #define SCRIPTEDIT_H -#include +#include #include +#include #include "../../model/world/universalid.hpp" +#include "scripthighlighter.hpp" + class QWidget; class QRegExp; @@ -16,11 +19,42 @@ namespace CSMDoc namespace CSVWorld { - class ScriptEdit : public QTextEdit + class ScriptEdit : public QPlainTextEdit { Q_OBJECT + public: - ScriptEdit (QWidget* parent, const CSMDoc::Document& document); + + class ChangeLock + { + ScriptEdit& mEdit; + + ChangeLock (const ChangeLock&); + ChangeLock& operator= (const ChangeLock&); + + public: + + ChangeLock (ScriptEdit& edit); + ~ChangeLock(); + }; + + friend class ChangeLock; + + private: + + int mChangeLocked; + ScriptHighlighter *mHighlighter; + QTimer mUpdateTimer; + + public: + + ScriptEdit (const CSMDoc::Document& document, ScriptHighlighter::Mode mode, + QWidget* parent); + + /// Should changes to the data be ignored (i.e. not cause updated)? + /// + /// \note This mechanism is used to avoid infinite update recursions + bool isChangeLocked() const; private: QVector mAllowedTypes; @@ -34,6 +68,12 @@ namespace CSVWorld void dragMoveEvent (QDragMoveEvent* event); bool stringNeedsQuote(const std::string& id) const; + + private slots: + + void idListChanged(); + + void updateHighlighting(); }; } #endif // SCRIPTEDIT_H \ No newline at end of file diff --git a/apps/opencs/view/world/scripthighlighter.cpp b/apps/opencs/view/world/scripthighlighter.cpp index e06dab372..36cebcb76 100644 --- a/apps/opencs/view/world/scripthighlighter.cpp +++ b/apps/opencs/view/world/scripthighlighter.cpp @@ -30,6 +30,16 @@ bool CSVWorld::ScriptHighlighter::parseName (const std::string& name, const Comp bool CSVWorld::ScriptHighlighter::parseKeyword (int keyword, const Compiler::TokenLoc& loc, Compiler::Scanner& scanner) { + if (((mMode==Mode_Console || mMode==Mode_Dialogue) && + (keyword==Compiler::Scanner::K_begin || keyword==Compiler::Scanner::K_end || + keyword==Compiler::Scanner::K_short || keyword==Compiler::Scanner::K_long || + keyword==Compiler::Scanner::K_float)) + || (mMode==Mode_Console && (keyword==Compiler::Scanner::K_if || + keyword==Compiler::Scanner::K_endif || keyword==Compiler::Scanner::K_else || + keyword==Compiler::Scanner::K_elseif || keyword==Compiler::Scanner::K_while || + keyword==Compiler::Scanner::K_endwhile))) + return parseName (loc.mLiteral, loc, scanner); + highlight (loc, Type_Keyword); return true; } @@ -63,8 +73,10 @@ void CSVWorld::ScriptHighlighter::highlight (const Compiler::TokenLoc& loc, Type setFormat (index, length, mScheme[type]); } -CSVWorld::ScriptHighlighter::ScriptHighlighter (const CSMWorld::Data& data, QTextDocument *parent) -: QSyntaxHighlighter (parent), Compiler::Parser (mErrorHandler, mContext), mContext (data) +CSVWorld::ScriptHighlighter::ScriptHighlighter (const CSMWorld::Data& data, Mode mode, + QTextDocument *parent) +: QSyntaxHighlighter (parent), Compiler::Parser (mErrorHandler, mContext), mContext (data), + mMode (mode) { /// \todo replace this with user settings { diff --git a/apps/opencs/view/world/scripthighlighter.hpp b/apps/opencs/view/world/scripthighlighter.hpp index 495c2e6a3..953f2f953 100644 --- a/apps/opencs/view/world/scripthighlighter.hpp +++ b/apps/opencs/view/world/scripthighlighter.hpp @@ -28,12 +28,20 @@ namespace CSVWorld Type_Id }; + enum Mode + { + Mode_General, + Mode_Console, + Mode_Dialogue + }; + private: Compiler::NullErrorHandler mErrorHandler; Compiler::Extensions mExtensions; CSMWorld::ScriptContext mContext; std::map mScheme; + Mode mMode; private: @@ -74,7 +82,7 @@ namespace CSVWorld public: - ScriptHighlighter (const CSMWorld::Data& data, QTextDocument *parent); + ScriptHighlighter (const CSMWorld::Data& data, Mode mode, QTextDocument *parent); virtual void highlightBlock (const QString& text); diff --git a/apps/opencs/view/world/scriptsubview.cpp b/apps/opencs/view/world/scriptsubview.cpp index fa41151ca..9b50a61f8 100644 --- a/apps/opencs/view/world/scriptsubview.cpp +++ b/apps/opencs/view/world/scriptsubview.cpp @@ -3,8 +3,6 @@ #include -#include - #include "../../model/doc/document.hpp" #include "../../model/world/universalid.hpp" #include "../../model/world/data.hpp" @@ -12,28 +10,12 @@ #include "../../model/world/commands.hpp" #include "../../model/world/idtable.hpp" -#include "scripthighlighter.hpp" #include "scriptedit.hpp" -CSVWorld::ScriptSubView::ChangeLock::ChangeLock (ScriptSubView& view) : mView (view) -{ - ++mView.mChangeLocked; -} - -CSVWorld::ScriptSubView::ChangeLock::~ChangeLock() -{ - --mView.mChangeLocked; -} - CSVWorld::ScriptSubView::ScriptSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document) -: SubView (id), mDocument (document), mColumn (-1), mChangeLocked (0) +: SubView (id), mDocument (document), mColumn (-1) { - setWidget (mEditor = new ScriptEdit (this, mDocument)); - - mEditor->setAcceptRichText (false); - mEditor->setLineWrapMode (QTextEdit::NoWrap); - mEditor->setTabStopWidth (4); - mEditor->setUndoRedoEnabled (false); // we use OpenCS-wide undo/redo instead + setWidget (mEditor = new ScriptEdit (mDocument, ScriptHighlighter::Mode_General, this)); mModel = &dynamic_cast ( *document.getData().getTableModel (CSMWorld::UniversalId::Type_Scripts)); @@ -58,14 +40,6 @@ CSVWorld::ScriptSubView::ScriptSubView (const CSMWorld::UniversalId& id, CSMDoc: connect (mModel, SIGNAL (rowsAboutToBeRemoved (const QModelIndex&, int, int)), this, SLOT (rowsAboutToBeRemoved (const QModelIndex&, int, int))); - - connect (&document.getData(), SIGNAL (idListChanged()), this, SLOT (idListChanged())); - - mHighlighter = new ScriptHighlighter (document.getData(), mEditor->document()); - - connect (&mUpdateTimer, SIGNAL (timeout()), this, SLOT (updateHighlighting())); - - mUpdateTimer.setSingleShot (true); } void CSVWorld::ScriptSubView::setEditLock (bool locked) @@ -73,20 +47,38 @@ void CSVWorld::ScriptSubView::setEditLock (bool locked) mEditor->setReadOnly (locked); } -void CSVWorld::ScriptSubView::idListChanged() +void CSVWorld::ScriptSubView::useHint (const std::string& hint) { - mHighlighter->invalidateIds(); + if (hint.empty()) + return; - if (!mUpdateTimer.isActive()) - mUpdateTimer.start (0); + if (hint[0]=='l') + { + std::istringstream stream (hint.c_str()+1); + + char ignore; + int line; + int column; + + if (stream >> ignore >> line >> column) + { + QTextCursor cursor = mEditor->textCursor(); + + cursor.movePosition (QTextCursor::Start); + if (cursor.movePosition (QTextCursor::Down, QTextCursor::MoveAnchor, line)) + cursor.movePosition (QTextCursor::Right, QTextCursor::MoveAnchor, column); + + mEditor->setTextCursor (cursor); + } + } } void CSVWorld::ScriptSubView::textChanged() { - if (mChangeLocked) + if (mEditor->isChangeLocked()) return; - ChangeLock lock (*this); + ScriptEdit::ChangeLock lock (*mEditor); mDocument.getUndoStack().push (new CSMWorld::ModifyCommand (*mModel, mModel->getModelIndex (getUniversalId().getId(), mColumn), mEditor->toPlainText())); @@ -94,10 +86,10 @@ void CSVWorld::ScriptSubView::textChanged() void CSVWorld::ScriptSubView::dataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) { - if (mChangeLocked) + if (mEditor->isChangeLocked()) return; - ChangeLock lock (*this); + ScriptEdit::ChangeLock lock (*mEditor); QModelIndex index = mModel->getModelIndex (getUniversalId().getId(), mColumn); @@ -115,15 +107,6 @@ void CSVWorld::ScriptSubView::rowsAboutToBeRemoved (const QModelIndex& parent, i QModelIndex index = mModel->getModelIndex (getUniversalId().getId(), mColumn); if (!parent.isValid() && index.row()>=start && index.row()<=end) - deleteLater(); + emit closeRequest(); } -void CSVWorld::ScriptSubView::updateHighlighting() -{ - if (mChangeLocked) - return; - - ChangeLock lock (*this); - - mHighlighter->rehighlight(); -} \ No newline at end of file diff --git a/apps/opencs/view/world/scriptsubview.hpp b/apps/opencs/view/world/scriptsubview.hpp index 7ceab70ba..16ffc7b80 100644 --- a/apps/opencs/view/world/scriptsubview.hpp +++ b/apps/opencs/view/world/scriptsubview.hpp @@ -3,9 +3,6 @@ #include "../doc/subview.hpp" -#include - -class QTextEdit; class QModelIndex; namespace CSMDoc @@ -20,34 +17,16 @@ namespace CSMWorld namespace CSVWorld { - class ScriptHighlighter; + class ScriptEdit; class ScriptSubView : public CSVDoc::SubView { Q_OBJECT - QTextEdit *mEditor; + ScriptEdit *mEditor; CSMDoc::Document& mDocument; CSMWorld::IdTable *mModel; int mColumn; - int mChangeLocked; - ScriptHighlighter *mHighlighter; - QTimer mUpdateTimer; - - class ChangeLock - { - ScriptSubView& mView; - - ChangeLock (const ChangeLock&); - ChangeLock& operator= (const ChangeLock&); - - public: - - ChangeLock (ScriptSubView& view); - ~ChangeLock(); - }; - - friend class ChangeLock; public: @@ -55,19 +34,15 @@ namespace CSVWorld virtual void setEditLock (bool locked); - public slots: + virtual void useHint (const std::string& hint); - void idListChanged(); + public slots: void textChanged(); void dataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); void rowsAboutToBeRemoved (const QModelIndex& parent, int start, int end); - - private slots: - - void updateHighlighting(); }; } diff --git a/apps/opencs/view/world/subviews.cpp b/apps/opencs/view/world/subviews.cpp index 200a26a85..5e01ef283 100644 --- a/apps/opencs/view/world/subviews.cpp +++ b/apps/opencs/view/world/subviews.cpp @@ -3,8 +3,6 @@ #include "../doc/subviewfactoryimp.hpp" -#include "../filter/filtercreator.hpp" - #include "tablesubview.hpp" #include "dialoguesubview.hpp" #include "scriptsubview.hpp" @@ -28,6 +26,9 @@ void CSVWorld::addSubViewFactories (CSVDoc::SubViewFactoryManager& manager) manager.add (CSMWorld::UniversalId::Type_Skills, new CSVDoc::SubViewFactoryWithCreator); + manager.add (CSMWorld::UniversalId::Type_MagicEffects, + new CSVDoc::SubViewFactoryWithCreator); + static const CSMWorld::UniversalId::Type sTableTypes[] = { CSMWorld::UniversalId::Type_Globals, @@ -35,12 +36,14 @@ void CSVWorld::addSubViewFactories (CSVDoc::SubViewFactoryManager& manager) CSMWorld::UniversalId::Type_Factions, CSMWorld::UniversalId::Type_Races, CSMWorld::UniversalId::Type_Sounds, - CSMWorld::UniversalId::Type_Scripts, CSMWorld::UniversalId::Type_Regions, CSMWorld::UniversalId::Type_Birthsigns, CSMWorld::UniversalId::Type_Spells, CSMWorld::UniversalId::Type_Enchantments, CSMWorld::UniversalId::Type_BodyParts, + CSMWorld::UniversalId::Type_SoundGens, + CSMWorld::UniversalId::Type_Pathgrids, + CSMWorld::UniversalId::Type_StartScripts, CSMWorld::UniversalId::Type_None // end marker }; @@ -91,11 +94,20 @@ void CSVWorld::addSubViewFactories (CSVDoc::SubViewFactoryManager& manager) // Other stuff (combined record tables) manager.add (CSMWorld::UniversalId::Type_RegionMap, new CSVDoc::SubViewFactory); + manager.add (CSMWorld::UniversalId::Type_Scene, new CSVDoc::SubViewFactory); + + // More other stuff manager.add (CSMWorld::UniversalId::Type_Filters, new CSVDoc::SubViewFactoryWithCreator >); + CreatorFactory >); - manager.add (CSMWorld::UniversalId::Type_Scene, new CSVDoc::SubViewFactory); + manager.add (CSMWorld::UniversalId::Type_DebugProfiles, + new CSVDoc::SubViewFactoryWithCreator >); + + manager.add (CSMWorld::UniversalId::Type_Scripts, + new CSVDoc::SubViewFactoryWithCreator >); // Dialogue subviews static const CSMWorld::UniversalId::Type sTableTypes2[] = @@ -106,11 +118,13 @@ void CSVWorld::addSubViewFactories (CSVDoc::SubViewFactoryManager& manager) CSMWorld::UniversalId::Type_Global, CSMWorld::UniversalId::Type_Race, CSMWorld::UniversalId::Type_Class, - CSMWorld::UniversalId::Type_Filter, CSMWorld::UniversalId::Type_Sound, CSMWorld::UniversalId::Type_Faction, CSMWorld::UniversalId::Type_Enchantment, CSMWorld::UniversalId::Type_BodyPart, + CSMWorld::UniversalId::Type_SoundGen, + CSMWorld::UniversalId::Type_Pathgrid, + CSMWorld::UniversalId::Type_StartScript, CSMWorld::UniversalId::Type_None // end marker }; @@ -123,6 +137,9 @@ void CSVWorld::addSubViewFactories (CSVDoc::SubViewFactoryManager& manager) manager.add (CSMWorld::UniversalId::Type_Skill, new CSVDoc::SubViewFactoryWithCreator (false)); + manager.add (CSMWorld::UniversalId::Type_MagicEffect, + new CSVDoc::SubViewFactoryWithCreator (false)); + manager.add (CSMWorld::UniversalId::Type_Gmst, new CSVDoc::SubViewFactoryWithCreator (false)); @@ -147,6 +164,12 @@ void CSVWorld::addSubViewFactories (CSVDoc::SubViewFactoryManager& manager) manager.add (CSMWorld::UniversalId::Type_Journal, new CSVDoc::SubViewFactoryWithCreator (false)); + manager.add (CSMWorld::UniversalId::Type_DebugProfile, + new CSVDoc::SubViewFactoryWithCreator > (false)); + + manager.add (CSMWorld::UniversalId::Type_Filter, + new CSVDoc::SubViewFactoryWithCreator > (false)); + //preview manager.add (CSMWorld::UniversalId::Type_Preview, new CSVDoc::SubViewFactory); } \ No newline at end of file diff --git a/apps/opencs/view/world/table.cpp b/apps/opencs/view/world/table.cpp index 43a34e41d..e864e4ed2 100644 --- a/apps/opencs/view/world/table.cpp +++ b/apps/opencs/view/world/table.cpp @@ -54,6 +54,35 @@ void CSVWorld::Table::contextMenuEvent (QContextMenuEvent *event) /// \todo add menu items for select all and clear selection + { + // Request UniversalId editing from table columns. + + int currRow = rowAt( event->y() ), + currCol = columnAt( event->x() ); + + currRow = mProxyModel->mapToSource(mProxyModel->index( currRow, 0 )).row(); + + CSMWorld::ColumnBase::Display colDisplay = + static_cast( + mModel->headerData( + currCol, + Qt::Horizontal, + CSMWorld::ColumnBase::Role_Display ).toInt()); + + QString cellData = mModel->data(mModel->index( currRow, currCol )).toString(); + CSMWorld::UniversalId::Type colType = CSMWorld::TableMimeData::convertEnums( colDisplay ); + + if ( !cellData.isEmpty() + && colType != CSMWorld::UniversalId::Type_None ) + { + mEditCellAction->setText(tr("Edit '").append(cellData).append("'")); + + menu.addAction( mEditCellAction ); + + mEditCellId = CSMWorld::UniversalId( colType, cellData.toUtf8().constData() ); + } + } + if (!mEditLock && !(mModel->getFeatures() & CSMWorld::IdTableBase::Feature_Constant)) { if (selectedRows.size()==1) @@ -148,6 +177,79 @@ void CSVWorld::Table::contextMenuEvent (QContextMenuEvent *event) menu.exec (event->globalPos()); } +void CSVWorld::Table::mouseDoubleClickEvent (QMouseEvent *event) +{ + Qt::KeyboardModifiers modifiers = + event->modifiers() & (Qt::ShiftModifier | Qt::ControlModifier); + + QModelIndex index = currentIndex(); + + selectionModel()->select (index, + QItemSelectionModel::Clear | QItemSelectionModel::Select | QItemSelectionModel::Rows); + + std::map::iterator iter = + mDoubleClickActions.find (modifiers); + + if (iter==mDoubleClickActions.end()) + { + event->accept(); + return; + } + + switch (iter->second) + { + case Action_None: + + event->accept(); + break; + + case Action_InPlaceEdit: + + DragRecordTable::mouseDoubleClickEvent (event); + break; + + case Action_EditRecord: + + event->accept(); + editRecord(); + break; + + case Action_View: + + event->accept(); + viewRecord(); + break; + + case Action_Revert: + + event->accept(); + if (mDispatcher->canRevert()) + mDispatcher->executeRevert(); + break; + + case Action_Delete: + + event->accept(); + if (mDispatcher->canDelete()) + mDispatcher->executeDelete(); + break; + + case Action_EditRecordAndClose: + + event->accept(); + editRecord(); + emit closeRequest(); + break; + + case Action_ViewAndClose: + + event->accept(); + viewRecord(); + emit closeRequest(); + break; + } +} + CSVWorld::Table::Table (const CSMWorld::UniversalId& id, bool createAndDelete, bool sorting, CSMDoc::Document& document) : mCreateAction (0), mCloneAction(0), mRecordStatusDisplay (0), @@ -179,7 +281,7 @@ CSVWorld::Table::Table (const CSMWorld::UniversalId& id, mModel->headerData (i, Qt::Horizontal, CSMWorld::ColumnBase::Role_Display).toInt()); CommandDelegate *delegate = CommandDelegateFactoryCollection::get().makeDelegate (display, - mDocument.getUndoStack(), this); + mDocument, this); mDelegates.push_back (delegate); setItemDelegateForColumn (i, delegate); @@ -219,6 +321,10 @@ CSVWorld::Table::Table (const CSMWorld::UniversalId& id, connect (mMoveDownAction, SIGNAL (triggered()), this, SLOT (moveDownRecord())); addAction (mMoveDownAction); + mEditCellAction = new QAction( tr("Edit Cell"), this ); + connect( mEditCellAction, SIGNAL(triggered()), this, SLOT(editCell()) ); + addAction( mEditCellAction ); + mViewAction = new QAction (tr ("View"), this); connect (mViewAction, SIGNAL (triggered()), this, SLOT (viewRecord())); addAction (mViewAction); @@ -251,6 +357,11 @@ CSVWorld::Table::Table (const CSMWorld::UniversalId& id, this, SLOT (selectionSizeUpdate ())); setAcceptDrops(true); + + mDoubleClickActions.insert (std::make_pair (Qt::NoModifier, Action_InPlaceEdit)); + mDoubleClickActions.insert (std::make_pair (Qt::ShiftModifier, Action_EditRecord)); + mDoubleClickActions.insert (std::make_pair (Qt::ControlModifier, Action_View)); + mDoubleClickActions.insert (std::make_pair (Qt::ShiftModifier | Qt::ControlModifier, Action_EditRecordAndClose)); } void CSVWorld::Table::setEditLock (bool locked) @@ -364,8 +475,16 @@ void CSVWorld::Table::moveDownRecord() } } +void CSVWorld::Table::editCell() +{ + emit editRequest( mEditCellId, std::string() ); +} + void CSVWorld::Table::viewRecord() { + if (!(mModel->getFeatures() & CSMWorld::IdTableBase::Feature_View)) + return; + QModelIndexList selectedRows = selectionModel()->selectedRows(); if (selectedRows.size()==1) @@ -401,18 +520,59 @@ void CSVWorld::Table::previewRecord() void CSVWorld::Table::updateUserSetting (const QString &name, const QStringList &list) { - int columns = mModel->columnCount(); + if (name=="records/type-format" || name=="records/status-format") + { + int columns = mModel->columnCount(); - for (int i=0; i - (*delegate).updateUserSetting (name, list); + for (int i=0; iindex (0, i), - mModel->index (mModel->rowCount()-1, i)); + dynamic_cast + (*delegate).updateUserSetting (name, list); + { + emit dataChanged (mModel->index (0, i), + mModel->index (mModel->rowCount()-1, i)); + } } - } + return; + } + + QString base ("table-input/double"); + if (name.startsWith (base)) + { + QString modifierString = name.mid (base.size()); + Qt::KeyboardModifiers modifiers = 0; + + if (modifierString=="-s") + modifiers = Qt::ShiftModifier; + else if (modifierString=="-c") + modifiers = Qt::ControlModifier; + else if (modifierString=="-sc") + modifiers = Qt::ShiftModifier | Qt::ControlModifier; + + DoubleClickAction action = Action_None; + + QString value = list.at (0); + + if (value=="Edit in Place") + action = Action_InPlaceEdit; + else if (value=="Edit Record") + action = Action_EditRecord; + else if (value=="View") + action = Action_View; + else if (value=="Revert") + action = Action_Revert; + else if (value=="Delete") + action = Action_Delete; + else if (value=="Edit Record and Close") + action = Action_EditRecordAndClose; + else if (value=="View and Close") + action = Action_ViewAndClose; + + mDoubleClickActions[modifiers] = action; + + return; + } } void CSVWorld::Table::tableSizeUpdate() @@ -448,12 +608,12 @@ void CSVWorld::Table::tableSizeUpdate() size = rows; } - tableSizeChanged (size, deleted, modified); + emit tableSizeChanged (size, deleted, modified); } void CSVWorld::Table::selectionSizeUpdate() { - selectionSizeChanged (selectionModel()->selectedRows().size()); + emit selectionSizeChanged (selectionModel()->selectedRows().size()); } void CSVWorld::Table::requestFocus (const std::string& id) @@ -467,6 +627,8 @@ void CSVWorld::Table::requestFocus (const std::string& id) void CSVWorld::Table::recordFilterChanged (boost::shared_ptr filter) { mProxyModel->setFilter (filter); + tableSizeUpdate(); + selectionSizeUpdate(); } void CSVWorld::Table::mouseMoveEvent (QMouseEvent* event) @@ -527,7 +689,6 @@ std::vector CSVWorld::Table::getColumnsWithDisplay(CSMWorld::Column std::vector< CSMWorld::UniversalId > CSVWorld::Table::getDraggedRecords() const { - QModelIndexList selectedRows = selectionModel()->selectedRows(); std::vector idToDrag; diff --git a/apps/opencs/view/world/table.hpp b/apps/opencs/view/world/table.hpp index 883834b60..75161b8b6 100644 --- a/apps/opencs/view/world/table.hpp +++ b/apps/opencs/view/world/table.hpp @@ -8,6 +8,7 @@ #include "../../model/filter/node.hpp" #include "../../model/world/columnbase.hpp" +#include "../../model/world/universalid.hpp" #include "dragrecordtable.hpp" class QUndoStack; @@ -21,7 +22,6 @@ namespace CSMDoc namespace CSMWorld { class Data; - class UniversalId; class IdTableProxyModel; class IdTableBase; class CommandDispatcher; @@ -36,6 +36,18 @@ namespace CSVWorld { Q_OBJECT + enum DoubleClickAction + { + Action_None, + Action_InPlaceEdit, + Action_EditRecord, + Action_View, + Action_Revert, + Action_Delete, + Action_EditRecordAndClose, + Action_ViewAndClose + }; + std::vector mDelegates; QAction *mEditAction; QAction *mCreateAction; @@ -45,6 +57,7 @@ namespace CSVWorld QAction *mMoveUpAction; QAction *mMoveDownAction; QAction *mViewAction; + QAction *mEditCellAction; QAction *mPreviewAction; QAction *mExtendedDeleteAction; QAction *mExtendedRevertAction; @@ -52,6 +65,8 @@ namespace CSVWorld CSMWorld::IdTableBase *mModel; int mRecordStatusDisplay; CSMWorld::CommandDispatcher *mDispatcher; + CSMWorld::UniversalId mEditCellId; + std::map mDoubleClickActions; private: @@ -61,6 +76,10 @@ namespace CSVWorld void dropEvent(QDropEvent *event); + protected: + + virtual void mouseDoubleClickEvent (QMouseEvent *event); + public: Table (const CSMWorld::UniversalId& id, bool createAndDelete, @@ -91,8 +110,12 @@ namespace CSVWorld void cloneRequest(const CSMWorld::UniversalId&); + void closeRequest(); + private slots: + void editCell(); + void editRecord(); void cloneRecord(); diff --git a/apps/opencs/view/world/tablesubview.cpp b/apps/opencs/view/world/tablesubview.cpp index 327fb1c0e..729b6b8d7 100644 --- a/apps/opencs/view/world/tablesubview.cpp +++ b/apps/opencs/view/world/tablesubview.cpp @@ -69,6 +69,8 @@ CSVWorld::TableSubView::TableSubView (const CSMWorld::UniversalId& id, CSMDoc::D connect(mFilterBox, SIGNAL(recordDropped(std::vector&, Qt::DropAction)), this, SLOT(createFilterRequest(std::vector&, Qt::DropAction))); + + connect (mTable, SIGNAL (closeRequest()), this, SLOT (closeRequest())); } void CSVWorld::TableSubView::setEditLock (bool locked) @@ -111,14 +113,21 @@ void CSVWorld::TableSubView::createFilterRequest (std::vector< CSMWorld::Univers { std::vector > > filterSource; + std::vector refIdColumns = mTable->getColumnsWithDisplay(CSMWorld::TableMimeData::convertEnums(CSMWorld::UniversalId::Type_Referenceable)); + bool hasRefIdDisplay = !refIdColumns.empty(); + for (std::vector::iterator it(types.begin()); it != types.end(); ++it) { - std::pair > pair( //splited long line - std::make_pair(it->getId(), mTable->getColumnsWithDisplay(CSMWorld::TableMimeData::convertEnums(it->getType())))); + CSMWorld::UniversalId::Type type = it->getType(); + std::vector col = mTable->getColumnsWithDisplay(CSMWorld::TableMimeData::convertEnums(type)); + if(!col.empty()) + { + filterSource.push_back(std::make_pair(it->getId(), col)); + } - if(!pair.second.empty()) + if(hasRefIdDisplay && CSMWorld::TableMimeData::isReferencable(type)) { - filterSource.push_back(pair); + filterSource.push_back(std::make_pair(it->getId(), refIdColumns)); } } @@ -129,17 +138,20 @@ bool CSVWorld::TableSubView::eventFilter (QObject* object, QEvent* event) { if (event->type() == QEvent::Drop) { - QDropEvent* drop = dynamic_cast(event); - const CSMWorld::TableMimeData* data = dynamic_cast(drop->mimeData()); - if (!data) // May happen when non-records (e.g. plain text) are dragged and dropped - return false; - - bool handled = data->holdsType(CSMWorld::UniversalId::Type_Filter); - if (handled) + if (QDropEvent* drop = dynamic_cast(event)) { - mFilterBox->setRecordFilter(data->returnMatching(CSMWorld::UniversalId::Type_Filter).getId()); + const CSMWorld::TableMimeData* data = dynamic_cast(drop->mimeData()); + if (!data) // May happen when non-records (e.g. plain text) are dragged and dropped + return false; + + bool handled = data->holdsType(CSMWorld::UniversalId::Type_Filter); + if (handled) + { + mFilterBox->setRecordFilter(data->returnMatching(CSMWorld::UniversalId::Type_Filter).getId()); + } + return handled; } - return handled; } return false; } + diff --git a/apps/opencs/view/world/util.cpp b/apps/opencs/view/world/util.cpp index eac8d3cb7..11521bb67 100644 --- a/apps/opencs/view/world/util.cpp +++ b/apps/opencs/view/world/util.cpp @@ -2,6 +2,8 @@ #include "util.hpp" #include +#include +#include #include #include @@ -17,6 +19,8 @@ #include "../../model/world/commands.hpp" #include "../../model/world/tablemimedata.hpp" +#include "scriptedit.hpp" + CSVWorld::NastyTableModelHack::NastyTableModelHack (QAbstractItemModel& model) : mModel (model) {} @@ -78,15 +82,15 @@ void CSVWorld::CommandDelegateFactoryCollection::add (CSMWorld::ColumnBase::Disp } CSVWorld::CommandDelegate *CSVWorld::CommandDelegateFactoryCollection::makeDelegate ( - CSMWorld::ColumnBase::Display display, QUndoStack& undoStack, QObject *parent) const + CSMWorld::ColumnBase::Display display, CSMDoc::Document& document, QObject *parent) const { std::map::const_iterator iter = mFactories.find (display); if (iter!=mFactories.end()) - return iter->second->makeDelegate (undoStack, parent); + return iter->second->makeDelegate (document, parent); - return new CommandDelegate (undoStack, parent); + return new CommandDelegate (document, parent); } const CSVWorld::CommandDelegateFactoryCollection& CSVWorld::CommandDelegateFactoryCollection::get() @@ -100,7 +104,12 @@ const CSVWorld::CommandDelegateFactoryCollection& CSVWorld::CommandDelegateFacto QUndoStack& CSVWorld::CommandDelegate::getUndoStack() const { - return mUndoStack; + return mDocument.getUndoStack(); +} + +CSMDoc::Document& CSVWorld::CommandDelegate::getDocument() const +{ + return mDocument; } void CSVWorld::CommandDelegate::setModelDataImp (QWidget *editor, QAbstractItemModel *model, @@ -112,11 +121,11 @@ void CSVWorld::CommandDelegate::setModelDataImp (QWidget *editor, QAbstractItemM QVariant new_ = hack.getData(); if (model->data (index)!=new_) - mUndoStack.push (new CSMWorld::ModifyCommand (*model, index, new_)); + getUndoStack().push (new CSMWorld::ModifyCommand (*model, index, new_)); } -CSVWorld::CommandDelegate::CommandDelegate (QUndoStack& undoStack, QObject *parent) -: QStyledItemDelegate (parent), mUndoStack (undoStack), mEditLock (false) +CSVWorld::CommandDelegate::CommandDelegate (CSMDoc::Document& document, QObject *parent) +: QStyledItemDelegate (parent), mDocument (document), mEditLock (false) {} void CSVWorld::CommandDelegate::setModelData (QWidget *editor, QAbstractItemModel *model, @@ -150,21 +159,32 @@ QWidget *CSVWorld::CommandDelegate::createEditor (QWidget *parent, const QStyleO return new QLineEdit(parent); case CSMWorld::ColumnBase::Display_Integer: - - return new QSpinBox(parent); + { + QSpinBox *sb = new QSpinBox(parent); + sb->setRange(INT_MIN, INT_MAX); + return sb; + } case CSMWorld::ColumnBase::Display_Var: return new QLineEdit(parent); case CSMWorld::ColumnBase::Display_Float: - - return new QDoubleSpinBox(parent); + { + QDoubleSpinBox *dsb = new QDoubleSpinBox(parent); + dsb->setRange(FLT_MIN, FLT_MAX); + dsb->setSingleStep(0.01f); + dsb->setDecimals(3); + return dsb; + } case CSMWorld::ColumnBase::Display_LongString: + { + QPlainTextEdit *edit = new QPlainTextEdit(parent); + edit->setUndoRedoEnabled (false); + return edit; + } - return new QTextEdit(parent); - case CSMWorld::ColumnBase::Display_Boolean: return new QCheckBox(parent); @@ -173,19 +193,25 @@ QWidget *CSVWorld::CommandDelegate::createEditor (QWidget *parent, const QStyleO case CSMWorld::ColumnBase::Display_Skill: case CSMWorld::ColumnBase::Display_Script: case CSMWorld::ColumnBase::Display_Race: + case CSMWorld::ColumnBase::Display_Region: case CSMWorld::ColumnBase::Display_Class: case CSMWorld::ColumnBase::Display_Faction: case CSMWorld::ColumnBase::Display_Miscellaneous: + case CSMWorld::ColumnBase::Display_Sound: case CSMWorld::ColumnBase::Display_Mesh: case CSMWorld::ColumnBase::Display_Icon: case CSMWorld::ColumnBase::Display_Music: case CSMWorld::ColumnBase::Display_SoundRes: case CSMWorld::ColumnBase::Display_Texture: case CSMWorld::ColumnBase::Display_Video: - case CSMWorld::ColumnBase::Display_Sound: + case CSMWorld::ColumnBase::Display_GlobalVariable: return new DropLineEdit(parent); + case CSMWorld::ColumnBase::Display_ScriptLines: + + return new ScriptEdit (mDocument, ScriptHighlighter::Mode_Console, parent); + default: return QStyledItemDelegate::createEditor (parent, option, index); diff --git a/apps/opencs/view/world/util.hpp b/apps/opencs/view/world/util.hpp index c95a24982..b4d972bf3 100644 --- a/apps/opencs/view/world/util.hpp +++ b/apps/opencs/view/world/util.hpp @@ -51,7 +51,8 @@ namespace CSVWorld virtual ~CommandDelegateFactory(); - virtual CommandDelegate *makeDelegate (QUndoStack& undoStack, QObject *parent) const = 0; + virtual CommandDelegate *makeDelegate (CSMDoc::Document& document, QObject *parent) + const = 0; ///< The ownership of the returned CommandDelegate is transferred to the caller. }; @@ -77,7 +78,7 @@ namespace CSVWorld /// /// This function must not be called more than once per value of \a display. - CommandDelegate *makeDelegate (CSMWorld::ColumnBase::Display display, QUndoStack& undoStack, + CommandDelegate *makeDelegate (CSMWorld::ColumnBase::Display display, CSMDoc::Document& document, QObject *parent) const; ///< The ownership of the returned CommandDelegate is transferred to the caller. /// @@ -110,19 +111,21 @@ namespace CSVWorld { Q_OBJECT - QUndoStack& mUndoStack; + CSMDoc::Document& mDocument; bool mEditLock; protected: QUndoStack& getUndoStack() const; + CSMDoc::Document& getDocument() const; + virtual void setModelDataImp (QWidget *editor, QAbstractItemModel *model, const QModelIndex& index) const; public: - CommandDelegate (QUndoStack& undoStack, QObject *parent); + CommandDelegate (CSMDoc::Document& document, QObject *parent); virtual void setModelData (QWidget *editor, QAbstractItemModel *model, const QModelIndex& index) const; diff --git a/apps/opencs/view/world/vartypedelegate.cpp b/apps/opencs/view/world/vartypedelegate.cpp index fc00f4491..c3c98b800 100644 --- a/apps/opencs/view/world/vartypedelegate.cpp +++ b/apps/opencs/view/world/vartypedelegate.cpp @@ -47,8 +47,8 @@ void CSVWorld::VarTypeDelegate::addCommands (QAbstractItemModel *model, const QM } CSVWorld::VarTypeDelegate::VarTypeDelegate (const std::vector >& values, - QUndoStack& undoStack, QObject *parent) -: EnumDelegate (values, undoStack, parent) + CSMDoc::Document& document, QObject *parent) +: EnumDelegate (values, document, parent) {} @@ -68,10 +68,10 @@ CSVWorld::VarTypeDelegateFactory::VarTypeDelegateFactory (ESM::VarType type0, add (type3); } -CSVWorld::CommandDelegate *CSVWorld::VarTypeDelegateFactory::makeDelegate (QUndoStack& undoStack, - QObject *parent) const +CSVWorld::CommandDelegate *CSVWorld::VarTypeDelegateFactory::makeDelegate ( + CSMDoc::Document& document, QObject *parent) const { - return new VarTypeDelegate (mValues, undoStack, parent); + return new VarTypeDelegate (mValues, document, parent); } void CSVWorld::VarTypeDelegateFactory::add (ESM::VarType type) diff --git a/apps/opencs/view/world/vartypedelegate.hpp b/apps/opencs/view/world/vartypedelegate.hpp index c8493f029..c86b936f6 100644 --- a/apps/opencs/view/world/vartypedelegate.hpp +++ b/apps/opencs/view/world/vartypedelegate.hpp @@ -17,7 +17,7 @@ namespace CSVWorld public: VarTypeDelegate (const std::vector >& values, - QUndoStack& undoStack, QObject *parent); + CSMDoc::Document& document, QObject *parent); }; class VarTypeDelegateFactory : public CommandDelegateFactory @@ -30,7 +30,7 @@ namespace CSVWorld ESM::VarType type1 = ESM::VT_Unknown, ESM::VarType type2 = ESM::VT_Unknown, ESM::VarType type3 = ESM::VT_Unknown); - virtual CommandDelegate *makeDelegate (QUndoStack& undoStack, QObject *parent) const; + virtual CommandDelegate *makeDelegate (CSMDoc::Document& document, QObject *parent) const; ///< The ownership of the returned CommandDelegate is transferred to the caller. void add (ESM::VarType type); diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index 0dda1131b..eabbb7577 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -2,8 +2,15 @@ set(GAME main.cpp engine.cpp + + ${CMAKE_SOURCE_DIR}/files/windows/openmw.rc ) -if(NOT WIN32) + +if (ANDROID) + set(GAME ${GAME} android_main.c) +endif() + +if(NOT WIN32 AND NOT ANDROID) set(GAME ${GAME} crashcatcher.cpp) endif() set(GAME_HEADER @@ -14,8 +21,8 @@ source_group(game FILES ${GAME} ${GAME_HEADER}) add_openmw_dir (mwrender renderingmanager debugging sky camera animation npcanimation creatureanimation activatoranimation actors objects renderinginterface localmap occlusionquery water shadows - characterpreview globalmap videoplayer ripplesimulation refraction - terrainstorage renderconst effectmanager weaponanimation terraingrid + characterpreview globalmap ripplesimulation refraction + terrainstorage renderconst effectmanager weaponanimation ) add_openmw_dir (mwinput @@ -25,19 +32,20 @@ add_openmw_dir (mwinput add_openmw_dir (mwgui textinput widgets race class birth review windowmanagerimp console dialogue windowbase statswindow messagebox journalwindow charactercreation - mapwindow windowpinnablebase tooltips scrollwindow bookwindow list + mapwindow windowpinnablebase tooltips scrollwindow bookwindow formatting inventorywindow container hud countdialog tradewindow settingswindow confirmationdialog alchemywindow referenceinterface spellwindow mainmenu quickkeysmenu itemselection spellbuyingwindow loadingscreen levelupdialog waitdialog spellcreationdialog - enchantingdialog trainingwindow travelwindow imagebutton exposedwindow cursor spellicons + enchantingdialog trainingwindow travelwindow exposedwindow cursor spellicons merchantrepair repair soulgemdialog companionwindow bookpage journalviewmodel journalbooks - keywordsearch itemmodel containeritemmodel inventoryitemmodel sortfilteritemmodel itemview - tradeitemmodel companionitemmodel pickpocketitemmodel fontloader controllers savegamedialog - recharge mode videowidget backgroundimage itemwidget + itemmodel containeritemmodel inventoryitemmodel sortfilteritemmodel itemview + tradeitemmodel companionitemmodel pickpocketitemmodel controllers savegamedialog + recharge mode videowidget backgroundimage itemwidget screenfader debugwindow spellmodel spellview + draganddrop timeadvancer jailscreen ) add_openmw_dir (mwdialogue - dialoguemanagerimp journalimp journalentry quest topic filter selectwrapper + dialoguemanagerimp journalimp journalentry quest topic filter selectwrapper hypertextparser keywordsearch scripttest ) add_openmw_dir (mwscript @@ -48,7 +56,7 @@ add_openmw_dir (mwscript ) add_openmw_dir (mwsound - soundmanagerimp openal_output ffmpeg_decoder sound + soundmanagerimp openal_output ffmpeg_decoder sound sound_decoder sound_output loudness libavwrapper movieaudiofactory ) add_openmw_dir (mwworld @@ -57,7 +65,7 @@ add_openmw_dir (mwworld cells localscripts customdata weather inventorystore ptr actionopen actionread actionequip timestamp actionalchemy cellstore actionapply actioneat esmstore store recordcmp fallback actionrepair actionsoulgem livecellref actiondoor - contentloader esmloader omwloader actiontrap cellreflist projectilemanager cellref + contentloader esmloader actiontrap cellreflist projectilemanager cellref ) add_openmw_dir (mwclass @@ -69,7 +77,7 @@ add_openmw_dir (mwmechanics mechanicsmanagerimp stat character creaturestats magiceffects movement actors objects drawstate spells activespells npcstats aipackage aisequence aipursue alchemy aiwander aitravel aifollow aiavoiddoor aiescort aiactivate aicombat repair enchanting pathfinding pathgrid security spellsuccess spellcasting - disease pickpocket levelledlist combat steering obstacle autocalcspell difficultyscaling + disease pickpocket levelledlist combat steering obstacle autocalcspell difficultyscaling aicombataction actor summoning ) add_openmw_dir (mwstate @@ -82,24 +90,37 @@ add_openmw_dir (mwbase ) # Main executable -set(BOOST_COMPONENTS system filesystem program_options thread wave) +if (ANDROID) + set(BOOST_COMPONENTS system filesystem program_options thread wave atomic) +else () + set(BOOST_COMPONENTS system filesystem program_options thread wave) +endif () + if(WIN32) set(BOOST_COMPONENTS ${BOOST_COMPONENTS} locale) endif(WIN32) find_package(Boost REQUIRED COMPONENTS ${BOOST_COMPONENTS}) -add_executable(openmw - ${OPENMW_LIBS} ${OPENMW_LIBS_HEADER} - ${OPENMW_FILES} - ${GAME} ${GAME_HEADER} - ${APPLE_BUNDLE_RESOURCES} -) +if (NOT ANDROID) + add_executable(openmw + ${OPENMW_LIBS} ${OPENMW_LIBS_HEADER} + ${OPENMW_FILES} + ${GAME} ${GAME_HEADER} + ${APPLE_BUNDLE_RESOURCES} + ) +else () + add_library(openmw + SHARED + ${OPENMW_LIBS} ${OPENMW_LIBS_HEADER} + ${OPENMW_FILES} + ${GAME} ${GAME_HEADER} + ) +endif () # Sound stuff - here so CMake doesn't stupidly recompile EVERYTHING # when we change the backend. include_directories(${SOUND_INPUT_INCLUDES} ${BULLET_INCLUDE_DIRS}) -add_definitions(${SOUND_DEFINE}) target_link_libraries(openmw ${OGRE_LIBRARIES} @@ -112,11 +133,29 @@ target_link_libraries(openmw ${MYGUI_LIBRARIES} ${SDL2_LIBRARY} ${MYGUI_PLATFORM_LIBRARIES} + "ogre-ffmpeg-videoplayer" "oics" "sdl4ogre" components ) +if (ANDROID) + target_link_libraries(openmw + ${OGRE_STATIC_PLUGINS} + EGL + android + log + dl + MyGUI.OgrePlatform + MyGUIEngineStatic + Plugin_StrangeButtonStatic + cpufeatures + BulletCollision + BulletDynamics + LinearMath + ) +endif (ANDROID) + if (USE_SYSTEM_TINYXML) target_link_libraries(openmw ${TINYXML_LIBRARIES}) endif() diff --git a/apps/openmw/android_main.c b/apps/openmw/android_main.c new file mode 100644 index 000000000..76da91c4f --- /dev/null +++ b/apps/openmw/android_main.c @@ -0,0 +1,43 @@ + +#include "../../SDL_internal.h" + +#ifdef __ANDROID__ +#include "SDL_main.h" + + +/******************************************************************************* + Functions called by JNI +*******************************************************************************/ +#include + +/* Called before to initialize JNI bindings */ + + + +extern void SDL_Android_Init(JNIEnv* env, jclass cls); + + +int Java_org_libsdl_app_SDLActivity_nativeInit(JNIEnv* env, jclass cls, jobject obj) +{ + + SDL_Android_Init(env, cls); + + SDL_SetMainReady(); + + +/* Run the application code! */ + + int status; + char *argv[2]; + argv[0] = SDL_strdup("openmw"); + argv[1] = NULL; + status = main(1, argv); + + /* Do not issue an exit or the whole application will terminate instead of just the SDL thread */ + /* exit(status); */ + + return status; +} + +#endif /* __ANDROID__ */ + diff --git a/apps/openmw/crashcatcher.cpp b/apps/openmw/crashcatcher.cpp index 75d2d7953..8f25d041c 100644 --- a/apps/openmw/crashcatcher.cpp +++ b/apps/openmw/crashcatcher.cpp @@ -11,7 +11,6 @@ #include #include - #include #include #include @@ -29,6 +28,7 @@ #include #endif +#define UNUSED(x) (void)(x) static const char crash_switch[] = "--cc-handle-crash"; @@ -37,6 +37,10 @@ static const char pipe_err[] = "!!! Failed to create pipe\n"; static const char fork_err[] = "!!! Failed to fork debug process\n"; static const char exec_err[] = "!!! Failed to exec debug process\n"; +#ifndef PATH_MAX /* Not all platforms (GNU Hurd) have this. */ +# define PATH_MAX 256 +#endif + static char argv0[PATH_MAX]; static char altstack[SIGSTKSZ]; @@ -66,7 +70,7 @@ static const struct { int code; const char *name; } sigill_codes[] = { - #ifndef __FreeBSD__ + #if !defined(__FreeBSD__) && !defined(__FreeBSD_kernel__) { ILL_ILLOPC, "Illegal opcode" }, { ILL_ILLOPN, "Illegal operand" }, { ILL_ILLADR, "Illegal addressing mode" }, @@ -123,7 +127,6 @@ static int (*cc_user_info)(char*, char*); static void gdb_info(pid_t pid) { char respfile[64]; - char cmd_buf[128]; FILE *f; int fd; @@ -152,11 +155,16 @@ static void gdb_info(pid_t pid) fclose(f); /* Run gdb and print process info. */ + char cmd_buf[128]; snprintf(cmd_buf, sizeof(cmd_buf), "gdb --quiet --batch --command=%s", respfile); printf("Executing: %s\n", cmd_buf); fflush(stdout); - system(cmd_buf); + { /* another special exception for "ignoring return value..." */ + int unused; + unused = system(cmd_buf); + UNUSED(unused); + } /* Clean up */ remove(respfile); } @@ -402,7 +410,13 @@ int cc_install_handlers(int argc, char **argv, int num_signals, int *signals, co snprintf(argv0, sizeof(argv0), "%s", argv[0]); else { - getcwd(argv0, sizeof(argv0)); + { + /* we don't want to disable "ignoring return value" warnings, so we make + * a special exception here. */ + char * unused; + unused = getcwd(argv0, sizeof(argv0)); + UNUSED(unused); + } retval = strlen(argv0); snprintf(argv0+retval, sizeof(argv0)-retval, "/%s", argv[0]); } diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index f8b4c9856..96cebd024 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -15,7 +15,6 @@ #include #include #include -#include #include #include @@ -41,6 +40,7 @@ #include "mwdialogue/dialoguemanagerimp.hpp" #include "mwdialogue/journalimp.hpp" +#include "mwdialogue/scripttest.hpp" #include "mwmechanics/mechanicsmanagerimp.hpp" @@ -80,13 +80,17 @@ bool OMW::Engine::frameRenderingQueued (const Ogre::FrameEvent& evt) { try { - float frametime = std::min(evt.timeSinceLastFrame, 0.2f); - + float frametime = evt.timeSinceLastFrame; mEnvironment.setFrameDuration (frametime); // update input MWBase::Environment::get().getInputManager()->update(frametime, false); + // When the window is minimized, pause everything. Currently this *has* to be here to work around a MyGUI bug. + // If we are not currently rendering, then RenderItems will not be reused resulting in a memory leak upon changing widget textures. + if (!mOgre->getWindow()->isActive() || !mOgre->getWindow()->isVisible()) + return true; + // sound if (mUseSound) MWBase::Environment::get().getSoundManager()->update(frametime); @@ -105,11 +109,14 @@ bool OMW::Engine::frameRenderingQueued (const Ogre::FrameEvent& evt) { if (!paused) { - // global scripts - MWBase::Environment::get().getScriptManager()->getGlobalScripts().run(); + if (MWBase::Environment::get().getWorld()->getScriptsEnabled()) + { + // local scripts + executeLocalScripts(); - // local scripts - executeLocalScripts(); + // global scripts + MWBase::Environment::get().getScriptManager()->getGlobalScripts().run(); + } MWBase::Environment::get().getWorld()->markCellAsUnchanged(); } @@ -166,11 +173,11 @@ bool OMW::Engine::frameRenderingQueued (const Ogre::FrameEvent& evt) OMW::Engine::Engine(Files::ConfigurationManager& configurationManager) : mOgre (0) - , mFpsLevel(0) , mVerboseScripts (false) , mSkipMenu (false) , mUseSound (true) , mCompileAll (false) + , mCompileAllDialogue (false) , mWarningsMode (1) , mScriptContext (0) , mFSStrict (false) @@ -180,12 +187,14 @@ OMW::Engine::Engine(Files::ConfigurationManager& configurationManager) , mEncoder(NULL) , mActivationDistanceOverride(-1) , mGrab(true) - + , mScriptBlacklistUse (true) + , mExportFonts(false) + , mNewGame (false) { std::srand ( std::time(NULL) ); MWClass::registerClasses(); - Uint32 flags = SDL_INIT_VIDEO|SDL_INIT_NOPARACHUTE; + Uint32 flags = SDL_INIT_VIDEO|SDL_INIT_NOPARACHUTE|SDL_INIT_GAMECONTROLLER|SDL_INIT_JOYSTICK; if(SDL_WasInit(flags) == 0) { //kindly ask SDL not to trash our OGL context @@ -201,6 +210,8 @@ OMW::Engine::Engine(Files::ConfigurationManager& configurationManager) OMW::Engine::~Engine() { + if (mOgre) + mOgre->restoreWindowGammaRamp(); mEnvironment.cleanup(); delete mScriptContext; delete mOgre; @@ -263,9 +274,10 @@ void OMW::Engine::setScriptsVerbosity(bool scriptsVerbosity) mVerboseScripts = scriptsVerbosity; } -void OMW::Engine::setSkipMenu (bool skipMenu) +void OMW::Engine::setSkipMenu (bool skipMenu, bool newGame) { mSkipMenu = skipMenu; + mNewGame = newGame; } std::string OMW::Engine::loadSettings (Settings::Manager & settings) @@ -282,16 +294,10 @@ std::string OMW::Engine::loadSettings (Settings::Manager & settings) else throw std::runtime_error ("No default settings file found! Make sure the file \"settings-default.cfg\" was properly installed."); - // load user settings if they exist, otherwise just load the default settings as user settings + // load user settings if they exist const std::string settingspath = mCfgMgr.getUserConfigPath().string() + "/settings.cfg"; if (boost::filesystem::exists(settingspath)) settings.loadUser(settingspath); - else if (boost::filesystem::exists(localdefault)) - settings.loadUser(localdefault); - else if (boost::filesystem::exists(globaldefault)) - settings.loadUser(globaldefault); - - mFpsLevel = settings.getInt("fps", "HUD"); // load nif overrides NifOverrides::Overrides nifOverrides; @@ -314,8 +320,6 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings) mEnvironment.setStateManager ( new MWState::StateManager (mCfgMgr.getUserDataPath() / "saves", mContentFiles.at (0))); - Nif::NIFFile::CacheLock cachelock; - std::string renderSystem = settings.getString("render system", "Video"); if (renderSystem == "") { @@ -336,16 +340,16 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings) // This has to be added BEFORE MyGUI is initialized, as it needs // to find core.xml here. - //addResourcesDirectory(mResDir); - addResourcesDirectory(mCfgMgr.getCachePath ().string()); addResourcesDirectory(mResDir / "mygui"); addResourcesDirectory(mResDir / "water"); addResourcesDirectory(mResDir / "shadows"); + addResourcesDirectory(mResDir / "materials"); OEngine::Render::WindowSettings windowSettings; windowSettings.fullscreen = settings.getBool("fullscreen", "Video"); + windowSettings.window_border = settings.getBool("window border", "Video"); windowSettings.window_x = settings.getInt("resolution x", "Video"); windowSettings.window_y = settings.getInt("resolution y", "Video"); windowSettings.screen = settings.getInt("screen", "Video"); @@ -364,19 +368,41 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings) // Create input and UI first to set up a bootstrapping environment for // showing a loading screen and keeping the window responsive while doing so - std::string keybinderUser = (mCfgMgr.getUserConfigPath() / "input_v1.xml").string(); + std::string keybinderUser = (mCfgMgr.getUserConfigPath() / "input_v3.xml").string(); bool keybinderUserExists = boost::filesystem::exists(keybinderUser); - MWInput::InputManager* input = new MWInput::InputManager (*mOgre, *this, keybinderUser, keybinderUserExists, mGrab); + if(!keybinderUserExists) + { + std::string input2 = (mCfgMgr.getUserConfigPath() / "input_v2.xml").string(); + if(boost::filesystem::exists(input2)) { + boost::filesystem::copy_file(input2, keybinderUser); + keybinderUserExists = boost::filesystem::exists(keybinderUser); + } + } + + // find correct path to the game controller bindings + const std::string localdefault = mCfgMgr.getLocalPath().string() + "/gamecontrollerdb.cfg"; + const std::string globaldefault = mCfgMgr.getGlobalPath().string() + "/gamecontrollerdb.cfg"; + std::string gameControllerdb; + if (boost::filesystem::exists(localdefault)) + gameControllerdb = localdefault; + else if (boost::filesystem::exists(globaldefault)) + gameControllerdb = globaldefault; + else + gameControllerdb = ""; //if it doesn't exist, pass in an empty string + + MWInput::InputManager* input = new MWInput::InputManager (*mOgre, *this, keybinderUser, keybinderUserExists, gameControllerdb, mGrab); mEnvironment.setInputManager (input); MWGui::WindowManager* window = new MWGui::WindowManager( - mExtensions, mFpsLevel, mOgre, mCfgMgr.getLogPath().string() + std::string("/"), - mCfgMgr.getCachePath ().string(), mScriptConsoleMode, mTranslationDataStorage, mEncoding); + mExtensions, mOgre, mCfgMgr.getLogPath().string() + std::string("/"), + mCfgMgr.getCachePath ().string(), mScriptConsoleMode, mTranslationDataStorage, mEncoding, mExportFonts, mFallbackMap); mEnvironment.setWindowManager (window); // Create sound system mEnvironment.setSoundManager (new MWSound::SoundManager(mUseSound)); + mOgre->setWindowGammaContrast(Settings::Manager::getFloat("gamma", "General"), Settings::Manager::getFloat("contrast", "General")); + if (!mSkipMenu) { std::string logo = mFallbackMap["Movies_Company_Logo"]; @@ -406,7 +432,8 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings) mScriptContext->setExtensions (&mExtensions); mEnvironment.setScriptManager (new MWScript::ScriptManager (MWBase::Environment::get().getWorld()->getStore(), - mVerboseScripts, *mScriptContext, mWarningsMode)); + mVerboseScripts, *mScriptContext, mWarningsMode, + mScriptBlacklistUse ? mScriptBlacklist : std::vector())); // Create game mechanics system MWMechanics::MechanicsManager* mechanics = new MWMechanics::MechanicsManager; @@ -422,7 +449,6 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings) if (mCompileAll) { std::pair result = MWBase::Environment::get().getScriptManager()->compileAll(); - if (result.first) std::cout << "compiled " << result.second << " of " << result.first << " scripts (" @@ -430,6 +456,16 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings) << "%)" << std::endl; } + if (mCompileAllDialogue) + { + std::pair result = MWDialogue::ScriptTest::compileAll(&mExtensions, mWarningsMode); + if (result.first) + std::cout + << "compiled " << result.second << " of " << result.first << " dialogue script/actor combinations a(" + << 100*static_cast (result.second)/result.first + << "%)" + << std::endl; + } } // Initialise and enter main loop. @@ -453,9 +489,13 @@ void OMW::Engine::go() // Play some good 'ol tunes MWBase::Environment::get().getSoundManager()->playPlaylist(std::string("Explore")); - // start in main menu - if (!mSkipMenu) + if (!mSaveGameFile.empty()) + { + MWBase::Environment::get().getStateManager()->loadGame(mSaveGameFile); + } + else if (!mSkipMenu) { + // start in main menu MWBase::Environment::get().getWindowManager()->pushGuiMode (MWGui::GM_MainMenu); try { @@ -470,13 +510,19 @@ void OMW::Engine::go() } else { - MWBase::Environment::get().getStateManager()->newGame (true); + MWBase::Environment::get().getStateManager()->newGame (!mNewGame); } // Start the main rendering loop - while (!mEnvironment.get().getStateManager()->hasQuitRequest()) - Ogre::Root::getSingleton().renderOneFrame(); + Ogre::Timer timer; + while (!MWBase::Environment::get().getStateManager()->hasQuitRequest()) + { + float dt = timer.getMilliseconds()/1000.f; + dt = std::min(dt, 0.2f); + timer.reset(); + Ogre::Root::getSingleton().renderOneFrame(dt); + } // Save user settings settings.saveUser(settingspath); @@ -488,6 +534,11 @@ void OMW::Engine::activate() if (MWBase::Environment::get().getWindowManager()->isGuiMode()) return; + MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); + if (player.getClass().getCreatureStats(player).getMagicEffects().get(ESM::MagicEffect::Paralyze).getMagnitude() > 0 + || player.getClass().getCreatureStats(player).getKnockedDown()) + return; + MWWorld::Ptr ptr = MWBase::Environment::get().getWorld()->getFacedObject(); if (ptr.isEmpty()) @@ -496,6 +547,14 @@ void OMW::Engine::activate() if (ptr.getClass().getName(ptr) == "") // objects without name presented to user can never be activated return; + if (ptr.getClass().isActor()) + { + MWMechanics::CreatureStats &stats = ptr.getClass().getCreatureStats(ptr); + + if (stats.getAiSequence().isInCombat() && !stats.isDead()) + return; + } + MWBase::Environment::get().getWorld()->activate(ptr, MWBase::Environment::get().getWorld()->getPlayerPtr()); } @@ -505,7 +564,7 @@ void OMW::Engine::screenshot() int shotCount = 0; const std::string& screenshotPath = mCfgMgr.getUserDataPath().string(); - + std::string format = Settings::Manager::getString("screenshot format", "General"); // Find the first unused filename with a do-while std::ostringstream stream; do @@ -514,11 +573,11 @@ void OMW::Engine::screenshot() stream.str(""); stream.clear(); - stream << screenshotPath << "screenshot" << std::setw(3) << std::setfill('0') << shotCount++ << ".png"; + stream << screenshotPath << "screenshot" << std::setw(3) << std::setfill('0') << shotCount++ << "." << format; } while (boost::filesystem::exists(stream.str())); - mOgre->screenshot(stream.str()); + mOgre->screenshot(stream.str(), format); } void OMW::Engine::setCompileAll (bool all) @@ -526,14 +585,14 @@ void OMW::Engine::setCompileAll (bool all) mCompileAll = all; } -void OMW::Engine::setSoundUsage(bool soundUsage) +void OMW::Engine::setCompileAllDialogue (bool all) { - mUseSound = soundUsage; + mCompileAllDialogue = all; } -void OMW::Engine::showFPS(int level) +void OMW::Engine::setSoundUsage(bool soundUsage) { - mFpsLevel = level; + mUseSound = soundUsage; } void OMW::Engine::setEncoding(const ToUTF8::FromType& encoding) @@ -565,3 +624,23 @@ void OMW::Engine::setWarningsMode (int mode) { mWarningsMode = mode; } + +void OMW::Engine::setScriptBlacklist (const std::vector& list) +{ + mScriptBlacklist = list; +} + +void OMW::Engine::setScriptBlacklistUse (bool use) +{ + mScriptBlacklistUse = use; +} + +void OMW::Engine::enableFontExport(bool exportFonts) +{ + mExportFonts = exportFonts; +} + +void OMW::Engine::setSaveGameFile(const std::string &savegame) +{ + mSaveGameFile = savegame; +} diff --git a/apps/openmw/engine.hpp b/apps/openmw/engine.hpp index e0f51d0dc..3b088595c 100644 --- a/apps/openmw/engine.hpp +++ b/apps/openmw/engine.hpp @@ -7,6 +7,8 @@ #include #include #include +#include + #include "mwbase/environment.hpp" @@ -69,26 +71,34 @@ namespace OMW OEngine::Render::OgreRenderer *mOgre; std::string mCellName; std::vector mContentFiles; - int mFpsLevel; bool mVerboseScripts; bool mSkipMenu; bool mUseSound; bool mCompileAll; + bool mCompileAllDialogue; int mWarningsMode; std::string mFocusName; std::map mFallbackMap; bool mScriptConsoleMode; std::string mStartupScript; int mActivationDistanceOverride; + std::string mSaveGameFile; // Grab mouse? bool mGrab; + bool mExportFonts; + Compiler::Extensions mExtensions; Compiler::Context *mScriptContext; Files::Collections mFileCollections; bool mFSStrict; Translation::Storage mTranslationDataStorage; + std::vector mScriptBlacklist; + bool mScriptBlacklistUse; + bool mNewGame; + + Nif::Cache mNifCache; // not implemented Engine (const Engine&); @@ -140,16 +150,17 @@ namespace OMW */ void addContentFile(const std::string& file); - /// Enable fps counter - void showFPS(int level); - /// Enable or disable verbose script output void setScriptsVerbosity(bool scriptsVerbosity); /// Disable or enable all sounds void setSoundUsage(bool soundUsage); - void setSkipMenu (bool skipMenu); + /// Skip main menu and go directly into the game + /// + /// \param newGame Start a new game instead off dumping the player into the game + /// (ignored if !skipMenu). + void setSkipMenu (bool skipMenu, bool newGame); void setGrabMouse(bool grab) { mGrab = grab; } @@ -165,6 +176,9 @@ namespace OMW /// Compile all scripts (excludign dialogue scripts) at startup? void setCompileAll (bool all); + /// Compile all dialogue scripts at startup? + void setCompileAllDialogue (bool all); + /// Font encoding void setEncoding(const ToUTF8::FromType& encoding); @@ -181,6 +195,15 @@ namespace OMW void setWarningsMode (int mode); + void setScriptBlacklist (const std::vector& list); + + void setScriptBlacklistUse (bool use); + + void enableFontExport(bool exportFonts); + + /// Set the save game file to load after initialising the engine. + void setSaveGameFile(const std::string& savegame); + private: Files::ConfigurationManager& mCfgMgr; }; diff --git a/apps/openmw/main.cpp b/apps/openmw/main.cpp index adde408b9..82fda060e 100644 --- a/apps/openmw/main.cpp +++ b/apps/openmw/main.cpp @@ -62,7 +62,6 @@ void validate(boost::any &v, std::vector const &tokens, FallbackMap FallbackMap *map = boost::any_cast(&v); - std::map::iterator mapIt; for(std::vector::const_iterator it=tokens.begin(); it != tokens.end(); ++it) { int sep = it->find(","); @@ -76,7 +75,7 @@ void validate(boost::any &v, std::vector const &tokens, FallbackMap std::string key(it->substr(0,sep)); std::string value(it->substr(sep+1)); - if((mapIt = map->mMap.find(key)) == map->mMap.end()) + if(map->mMap.find(key) == map->mMap.end()) { map->mMap.insert(std::make_pair (key,value)); } @@ -105,7 +104,7 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat ("help", "print help message") ("version", "print version information and quit") ("data", bpo::value()->default_value(Files::PathContainer(), "data") - ->multitoken(), "set data directories (later directories have higher priority)") + ->multitoken()->composing(), "set data directories (later directories have higher priority)") ("data-local", bpo::value()->default_value(""), "set local data directory (highest priority)") @@ -131,6 +130,9 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat ("script-all", bpo::value()->implicit_value(true) ->default_value(false), "compile all scripts (excluding dialogue scripts) at startup") + ("script-all-dialogue", bpo::value()->implicit_value(true) + ->default_value(false), "compile all dialogue scripts at startup") + ("script-console", bpo::value()->implicit_value(true) ->default_value(false), "enable console-only script functionality") @@ -144,9 +146,21 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat "\t1 - show warning but consider script as correctly compiled anyway\n" "\t2 - treat warnings as errors") + ("script-blacklist", bpo::value()->default_value(StringsVector(), "") + ->multitoken(), "ignore the specified script (if the use of the blacklist is enabled)") + + ("script-blacklist-use", bpo::value()->implicit_value(true) + ->default_value(true), "enable script blacklisting") + + ("load-savegame", bpo::value()->default_value(""), + "load a save game file on game startup (specify an absolute filename or a filename relative to the current working directory)") + ("skip-menu", bpo::value()->implicit_value(true) ->default_value(false), "skip main menu on game startup") + ("new-game", bpo::value()->implicit_value(true) + ->default_value(false), "run new game sequence (ignored if skip-menu=0)") + ("fs-strict", bpo::value()->implicit_value(true) ->default_value(false), "strict file system handling (no case folding)") @@ -162,6 +176,9 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat ("no-grab", "Don't grab mouse cursor") + ("export-fonts", bpo::value()->implicit_value(true) + ->default_value(false), "Export Morrowind .fnt fonts to PNG image and XML file in current directory") + ("activate-dist", bpo::value ()->default_value (-1), "activation distance override"); bpo::parsed_options valid_opts = bpo::command_line_parser(argc, argv) @@ -173,21 +190,23 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat bpo::store(valid_opts, variables); bpo::notify(variables); - bool run = true; - if (variables.count ("help")) { std::cout << desc << std::endl; - run = false; + return false; } - if (variables.count ("version")) + std::cout << "OpenMW version " << OPENMW_VERSION; + std::string rev = OPENMW_VERSION_COMMITHASH; + std::string tag = OPENMW_VERSION_TAGHASH; + if (!rev.empty() && !tag.empty()) { - std::cout << "OpenMW version " << OPENMW_VERSION << std::endl; - run = false; + rev = rev.substr(0, 10); + std::cout << " (revision " << rev << ")"; } + std::cout << std::endl; - if (!run) + if (variables.count ("version")) return false; cfgMgr.readConfiguration(variables, desc); @@ -239,17 +258,26 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat // startup-settings engine.setCell(variables["start"].as()); - engine.setSkipMenu (variables["skip-menu"].as()); + engine.setSkipMenu (variables["skip-menu"].as(), variables["new-game"].as()); + if (!variables["skip-menu"].as() && variables["new-game"].as()) + std::cerr << "new-game used without skip-menu -> ignoring it" << std::endl; - // other settings - engine.setSoundUsage(!variables["no-sound"].as()); - engine.setScriptsVerbosity(variables["script-verbose"].as()); + // scripts engine.setCompileAll(variables["script-all"].as()); - engine.setFallbackValues(variables["fallback"].as().mMap); + engine.setCompileAllDialogue(variables["script-all-dialogue"].as()); + engine.setScriptsVerbosity(variables["script-verbose"].as()); engine.setScriptConsoleMode (variables["script-console"].as()); engine.setStartupScript (variables["script-run"].as()); - engine.setActivationDistanceOverride (variables["activate-dist"].as()); engine.setWarningsMode (variables["script-warn"].as()); + engine.setScriptBlacklist (variables["script-blacklist"].as()); + engine.setScriptBlacklistUse (variables["script-blacklist-use"].as()); + engine.setSaveGameFile (variables["load-savegame"].as()); + + // other settings + engine.setSoundUsage(!variables["no-sound"].as()); + engine.setFallbackValues(variables["fallback"].as().mMap); + engine.setActivationDistanceOverride (variables["activate-dist"].as()); + engine.enableFontExport(variables["export-fonts"].as()); return true; } @@ -309,6 +337,8 @@ int main(int argc, char**argv) boost::filesystem::ofstream logfile; + std::auto_ptr engine; + int ret = 0; try { @@ -350,22 +380,22 @@ int main(int argc, char**argv) boost::filesystem::current_path(bundlePath); #endif - OMW::Engine engine(cfgMgr); + engine.reset(new OMW::Engine(cfgMgr)); - if (parseOptions(argc, argv, engine, cfgMgr)) + if (parseOptions(argc, argv, *engine, cfgMgr)) { - engine.go(); + engine->go(); } } catch (std::exception &e) { #if OGRE_PLATFORM == OGRE_PLATFORM_LINUX || OGRE_PLATFORM == OGRE_PLATFORM_APPLE - if (isatty(fileno(stdin))) - std::cerr << "\nERROR: " << e.what() << std::endl; - else + if (!isatty(fileno(stdin))) #endif SDL_ShowSimpleMessageBox(0, "OpenMW: Fatal error", e.what(), NULL); + std::cerr << "\nERROR: " << e.what() << std::endl; + ret = 1; } diff --git a/apps/openmw/mwbase/dialoguemanager.hpp b/apps/openmw/mwbase/dialoguemanager.hpp index d0e64b23c..1fe63e633 100644 --- a/apps/openmw/mwbase/dialoguemanager.hpp +++ b/apps/openmw/mwbase/dialoguemanager.hpp @@ -69,11 +69,13 @@ namespace MWBase virtual void write (ESM::ESMWriter& writer, Loading::Listener& progress) const = 0; - virtual void readRecord (ESM::ESMReader& reader, int32_t type) = 0; + virtual void readRecord (ESM::ESMReader& reader, uint32_t type) = 0; /// Changes faction1's opinion of faction2 by \a diff. virtual void modFactionReaction (const std::string& faction1, const std::string& faction2, int diff) = 0; + virtual void setFactionReaction (const std::string& faction1, const std::string& faction2, int absolute) = 0; + /// @return faction1's opinion of faction2 virtual int getFactionReaction (const std::string& faction1, const std::string& faction2) const = 0; diff --git a/apps/openmw/mwbase/environment.hpp b/apps/openmw/mwbase/environment.hpp index eb636ea2f..7f7919f81 100644 --- a/apps/openmw/mwbase/environment.hpp +++ b/apps/openmw/mwbase/environment.hpp @@ -1,5 +1,5 @@ -#ifndef GAME_BASE_INVIRONMENT_H -#define GAME_BASE_INVIRONMENT_H +#ifndef GAME_BASE_ENVIRONMENT_H +#define GAME_BASE_ENVIRONMENT_H namespace MWBase { diff --git a/apps/openmw/mwbase/inputmanager.hpp b/apps/openmw/mwbase/inputmanager.hpp index d44da4974..79477d883 100644 --- a/apps/openmw/mwbase/inputmanager.hpp +++ b/apps/openmw/mwbase/inputmanager.hpp @@ -2,8 +2,8 @@ #define GAME_MWBASE_INPUTMANAGER_H #include - -#include +#include +#include namespace MWBase { @@ -29,7 +29,7 @@ namespace MWBase virtual void changeInputMode(bool guiMode) = 0; - virtual void processChangedSettings(const Settings::CategorySettingVector& changed) = 0; + virtual void processChangedSettings(const std::set< std::pair >& changed) = 0; virtual void setDragDrop(bool dragDrop) = 0; @@ -37,11 +37,23 @@ namespace MWBase virtual bool getControlSwitch (const std::string& sw) = 0; virtual std::string getActionDescription (int action) = 0; - virtual std::string getActionBindingName (int action) = 0; - virtual std::vector getActionSorting () = 0; + virtual std::string getActionKeyBindingName (int action) = 0; + virtual std::string getActionControllerBindingName (int action) = 0; + virtual std::string sdlControllerAxisToString(int axis) = 0; + virtual std::string sdlControllerButtonToString(int button) = 0; + ///Actions available for binding to keyboard buttons + virtual std::vector getActionKeySorting() = 0; + ///Actions available for binding to controller buttons + virtual std::vector getActionControllerSorting() = 0; virtual int getNumActions() = 0; - virtual void enableDetectingBindingMode (int action) = 0; - virtual void resetToDefaultBindings() = 0; + ///If keyboard is true, only pay attention to keyboard events. If false, only pay attention to controller events (excluding esc) + virtual void enableDetectingBindingMode (int action, bool keyboard) = 0; + virtual void resetToDefaultKeyBindings() = 0; + virtual void resetToDefaultControllerBindings() = 0; + + /// Returns if the last used input device was a joystick or a keyboard + /// @return true if joystick, false otherwise + virtual bool joystickLastUsed() = 0; }; } diff --git a/apps/openmw/mwbase/journal.hpp b/apps/openmw/mwbase/journal.hpp index 938cec74b..738014ba6 100644 --- a/apps/openmw/mwbase/journal.hpp +++ b/apps/openmw/mwbase/journal.hpp @@ -92,7 +92,7 @@ namespace MWBase virtual void write (ESM::ESMWriter& writer, Loading::Listener& progress) const = 0; - virtual void readRecord (ESM::ESMReader& reader, int32_t type) = 0; + virtual void readRecord (ESM::ESMReader& reader, uint32_t type) = 0; }; } diff --git a/apps/openmw/mwbase/mechanicsmanager.hpp b/apps/openmw/mwbase/mechanicsmanager.hpp index f718972b9..f7fc515f5 100644 --- a/apps/openmw/mwbase/mechanicsmanager.hpp +++ b/apps/openmw/mwbase/mechanicsmanager.hpp @@ -113,23 +113,25 @@ namespace MWBase OT_Theft, // Taking items owned by an NPC or a faction you are not a member of OT_Assault, // Attacking a peaceful NPC OT_Murder, // Murdering a peaceful NPC - OT_Trespassing, // Staying in a cell you are not allowed in (where is this defined?) + OT_Trespassing, // Picking the lock of an owned door/chest OT_SleepingInOwnedBed, // Sleeping in a bed owned by an NPC or a faction you are not a member of OT_Pickpocket // Entering pickpocket mode, leaving it, and being detected. Any items stolen are a separate crime (Theft) }; /** - * @brief Commit a crime. If any actors witness the crime and report it, - * reportCrime will be called automatically. * @note victim may be empty * @param arg Depends on \a type, e.g. for Theft, the value of the item that was stolen. - * @return was the crime reported? + * @param victimAware Is the victim already aware of the crime? + * If this parameter is false, it will be determined by a line-of-sight and awareness check. + * @return was the crime seen? */ virtual bool commitCrime (const MWWorld::Ptr& ptr, const MWWorld::Ptr& victim, - OffenseType type, int arg=0) = 0; - virtual void reportCrime (const MWWorld::Ptr& ptr, const MWWorld::Ptr& victim, - OffenseType type, int arg=0) = 0; + OffenseType type, int arg=0, bool victimAware=false) = 0; + /// @return false if the attack was considered a "friendly hit" and forgiven + virtual bool actorAttacked (const MWWorld::Ptr& victim, const MWWorld::Ptr& attacker) = 0; /// Utility to check if taking this item is illegal and calling commitCrime if so - virtual void itemTaken (const MWWorld::Ptr& ptr, const MWWorld::Ptr& item, int count) = 0; + /// @param container The container the item is in; may be empty for an item in the world + virtual void itemTaken (const MWWorld::Ptr& ptr, const MWWorld::Ptr& item, const MWWorld::Ptr& container, + int count) = 0; /// Utility to check if opening (i.e. unlocking) this object is illegal and calling commitCrime if so virtual void objectOpened (const MWWorld::Ptr& ptr, const MWWorld::Ptr& item) = 0; /// Attempt sleeping in a bed. If this is illegal, call commitCrime. @@ -178,6 +180,7 @@ namespace MWBase ///return the list of actors which are following the given actor /**ie AiFollow is active and the target is the actor**/ virtual std::list getActorsFollowing(const MWWorld::Ptr& actor) = 0; + virtual std::list getActorsFollowingIndices(const MWWorld::Ptr& actor) = 0; ///Returns a list of actors who are fighting the given actor within the fAlarmDistance /** ie AiCombat is active and the target is the actor **/ @@ -189,17 +192,25 @@ namespace MWBase virtual void write (ESM::ESMWriter& writer, Loading::Listener& listener) const = 0; - virtual void readRecord (ESM::ESMReader& reader, int32_t type) = 0; + virtual void readRecord (ESM::ESMReader& reader, uint32_t type) = 0; virtual void clear() = 0; - /// @param bias Can be used to add an additional aggression bias towards the target, - /// making it more likely for the function to return true. - virtual bool isAggressive (const MWWorld::Ptr& ptr, const MWWorld::Ptr& target, int bias=0, bool ignoreDistance=false) = 0; + virtual bool isAggressive (const MWWorld::Ptr& ptr, const MWWorld::Ptr& target) = 0; - /// Usually done once a frame, but can be invoked manually in time-critical situations. - /// This will increase the death count for any actors that were killed. - virtual void killDeadActors() = 0; + /// Resurrects the player if necessary + virtual void keepPlayerAlive() = 0; + + virtual bool isReadyToBlock (const MWWorld::Ptr& ptr) const = 0; + + virtual void confiscateStolenItems (const MWWorld::Ptr& player, const MWWorld::Ptr& targetContainer) = 0; + + /// List the owners that the player has stolen this item from (the owner can be an NPC or a faction). + /// + virtual std::vector > getStolenItemOwners(const std::string& itemid) = 0; + + /// Has the player stolen this item from the given owner? + virtual bool isItemStolenFrom(const std::string& itemid, const std::string& ownerid) = 0; }; } diff --git a/apps/openmw/mwbase/scriptmanager.hpp b/apps/openmw/mwbase/scriptmanager.hpp index ae146e064..7bdeba132 100644 --- a/apps/openmw/mwbase/scriptmanager.hpp +++ b/apps/openmw/mwbase/scriptmanager.hpp @@ -46,17 +46,11 @@ namespace MWBase ///< Compile all scripts /// \return count, success - virtual Compiler::Locals& getLocals (const std::string& name) = 0; + virtual const Compiler::Locals& getLocals (const std::string& name) = 0; ///< Return locals for script \a name. virtual MWScript::GlobalScripts& getGlobalScripts() = 0; - - virtual int getLocalIndex (const std::string& scriptId, const std::string& variable, - char type) = 0; - ///< Return index of the variable of the given name and type in the given script. Will - /// throw an exception, if there is no such script or variable or the type does not match. - - }; + }; } #endif diff --git a/apps/openmw/mwbase/soundmanager.hpp b/apps/openmw/mwbase/soundmanager.hpp index 15739730b..f3381a8fd 100644 --- a/apps/openmw/mwbase/soundmanager.hpp +++ b/apps/openmw/mwbase/soundmanager.hpp @@ -2,11 +2,9 @@ #define GAME_MWBASE_SOUNDMANAGER_H #include - +#include #include -#include - #include "../mwworld/ptr.hpp" namespace Ogre @@ -42,15 +40,21 @@ namespace MWBase Play_NoTrack = 1<<2, /* (3D only) Play the sound at the given object's position * but do not keep it updated (the sound will not move with * the object and will not stop when the object is deleted. */ - - Play_LoopNoEnv = Play_Loop | Play_NoEnv + Play_RemoveAtDistance = 1<<3, /* (3D only) If the listener gets further than 2000 units away + from the sound source, the sound is removed. + This is weird stuff but apparently how vanilla works for sounds + played by the PlayLoopSound family of script functions. Perhaps we + can make this cut off a more subtle fade later, but have to + be careful to not change the overall volume of areas by too much. */ + Play_LoopNoEnv = Play_Loop | Play_NoEnv, + Play_LoopRemoveAtDistance = Play_Loop | Play_RemoveAtDistance }; enum PlayType { - Play_TypeSfx = 1<<3, /* Normal SFX sound */ - Play_TypeVoice = 1<<4, /* Voice sound */ - Play_TypeFoot = 1<<5, /* Footstep sound */ - Play_TypeMusic = 1<<6, /* Music track */ - Play_TypeMovie = 1<<7, /* Movie audio track */ + Play_TypeSfx = 1<<4, /* Normal SFX sound */ + Play_TypeVoice = 1<<5, /* Voice sound */ + Play_TypeFoot = 1<<6, /* Footstep sound */ + Play_TypeMusic = 1<<7, /* Music track */ + Play_TypeMovie = 1<<8, /* Movie audio track */ Play_TypeMask = Play_TypeSfx|Play_TypeVoice|Play_TypeFoot|Play_TypeMusic|Play_TypeMovie }; @@ -68,7 +72,7 @@ namespace MWBase virtual ~SoundManager() {} - virtual void processChangedSettings(const Settings::CategorySettingVector& settings) = 0; + virtual void processChangedSettings(const std::set< std::pair >& settings) = 0; virtual void stopMusic() = 0; ///< Stops music if it's playing @@ -101,6 +105,11 @@ namespace MWBase virtual void stopSay(const MWWorld::Ptr &reference=MWWorld::Ptr()) = 0; ///< Stop an actor speaking + virtual float getSaySoundLoudness(const MWWorld::Ptr& reference) const = 0; + ///< Check the currently playing say sound for this actor + /// and get an average loudness value (scale [0,1]) at the current time position. + /// If the actor is not saying anything, returns 0. + virtual SoundPtr playTrack(const MWSound::DecoderPtr& decoder, PlayType type) = 0; ///< Play a 2D audio track, using a custom decoder diff --git a/apps/openmw/mwbase/statemanager.hpp b/apps/openmw/mwbase/statemanager.hpp index 006be921b..79ddbfa2a 100644 --- a/apps/openmw/mwbase/statemanager.hpp +++ b/apps/openmw/mwbase/statemanager.hpp @@ -62,10 +62,13 @@ namespace MWBase /// /// \note Slot must belong to the current character. - virtual void loadGame (const MWState::Character *character, const MWState::Slot *slot) = 0; - ///< Load a saved game file from \a slot. - /// - /// \note \a slot must belong to \a character. + virtual void loadGame (const std::string& filepath) = 0; + ///< Load a saved game directly from the given file path. This will search the CharacterManager + /// for a Character containing this save file, and set this Character current if one was found. + /// Otherwise, a new Character will be created. + + virtual void loadGame (const MWState::Character *character, const std::string& filepath) = 0; + ///< Load a saved game file belonging to the given character. ///Simple saver, writes over the file if already existing /** Used for quick save and autosave **/ diff --git a/apps/openmw/mwbase/windowmanager.hpp b/apps/openmw/mwbase/windowmanager.hpp index 8b407c9ba..2c4cb360d 100644 --- a/apps/openmw/mwbase/windowmanager.hpp +++ b/apps/openmw/mwbase/windowmanager.hpp @@ -1,19 +1,23 @@ #ifndef GAME_MWBASE_WINDOWMANAGER_H #define GAME_MWBASE_WINDOWMANAGER_H +#include #include #include #include +#include -#include - -#include - -#include +#include "../mwgui/mode.hpp" -#include "../mwmechanics/stat.hpp" +namespace Loading +{ + class Listener; +} -#include "../mwgui/mode.hpp" +namespace Translation +{ + class Storage; +} namespace MyGUI { @@ -35,6 +39,15 @@ namespace ESM struct Class; class ESMReader; class ESMWriter; + struct CellId; +} + +namespace MWMechanics +{ + class AttributeValue; + template + class DynamicStat; + class SkillValue; } namespace MWWorld @@ -58,6 +71,7 @@ namespace MWGui class ContainerWindow; class DialogueWindow; class WindowModal; + class JailScreen; enum ShowInDialogueMode { ShowInDialogueMode_IfPossible, @@ -109,6 +123,8 @@ namespace MWBase virtual void removeGuiMode (MWGui::GuiMode mode) = 0; ///< can be anywhere in the stack + virtual void goToJail(int days) = 0; + virtual void updatePlayer() = 0; virtual MWGui::GuiMode getMode() const = 0; @@ -145,8 +161,6 @@ namespace MWBase virtual MWGui::SpellWindow* getSpellWindow() = 0; virtual MWGui::Console* getConsole() = 0; - virtual MyGUI::Gui* getGui() const = 0; - virtual void wmUpdateFps(float fps, unsigned int triangleCount, unsigned int batchCount) = 0; /// Set value for the given ID. @@ -179,7 +193,7 @@ namespace MWBase virtual void changeCell(MWWorld::CellStore* cell) = 0; ///< change the active cell - virtual void setPlayerPos(const float x, const float y) = 0; + virtual void setPlayerPos(int cellX, int cellY, const float x, const float y) = 0; ///< set player position in map space virtual void setPlayerDir(const float x, const float y) = 0; @@ -227,7 +241,6 @@ namespace MWBase virtual void showCrosshair(bool show) = 0; virtual bool getSubtitlesEnabled() = 0; - virtual void toggleHud() = 0; virtual bool toggleGui() = 0; virtual void disallowMouse() = 0; @@ -243,9 +256,11 @@ namespace MWBase /** No guarentee of actually closing the window **/ virtual void exitCurrentGuiMode() = 0; - virtual void messageBox (const std::string& message, const std::vector& buttons = std::vector(), enum MWGui::ShowInDialogueMode showInDialogueMode = MWGui::ShowInDialogueMode_IfPossible) = 0; + virtual void messageBox (const std::string& message, enum MWGui::ShowInDialogueMode showInDialogueMode = MWGui::ShowInDialogueMode_IfPossible) = 0; virtual void staticMessageBox(const std::string& message) = 0; virtual void removeStaticMessageBox() = 0; + virtual void interactiveMessageBox (const std::string& message, + const std::vector& buttons = std::vector(), bool block=false) = 0; /// returns the index of the pressed button or -1 if no button was pressed (->MessageBoxmanager->InteractiveMessageBox) virtual int readPressedButton() = 0; @@ -267,7 +282,7 @@ namespace MWBase */ virtual std::string getGameSettingString(const std::string &id, const std::string &default_) = 0; - virtual void processChangedSettings(const Settings::CategorySettingVector& changed) = 0; + virtual void processChangedSettings(const std::set< std::pair >& changed) = 0; virtual void windowResized(int x, int y) = 0; @@ -311,7 +326,7 @@ namespace MWBase virtual void clear() = 0; virtual void write (ESM::ESMWriter& writer, Loading::Listener& progress) = 0; - virtual void readRecord (ESM::ESMReader& reader, int32_t type) = 0; + virtual void readRecord (ESM::ESMReader& reader, uint32_t type) = 0; virtual int countSavedGameRecords() const = 0; /// Does the current stack of GUI-windows permit saving? @@ -331,6 +346,25 @@ namespace MWBase virtual void removeCurrentModal(MWGui::WindowModal* input) = 0; virtual void pinWindow (MWGui::GuiWindow window) = 0; + + /// Fade the screen in, over \a time seconds + virtual void fadeScreenIn(const float time, bool clearQueue=true) = 0; + /// Fade the screen out to black, over \a time seconds + virtual void fadeScreenOut(const float time, bool clearQueue=true) = 0; + /// Fade the screen to a specified percentage of black, over \a time seconds + virtual void fadeScreenTo(const int percent, const float time, bool clearQueue=true) = 0; + /// Darken the screen to a specified percentage + virtual void setBlindness(const int percent) = 0; + + virtual void activateHitOverlay(bool interrupt=true) = 0; + virtual void setWerewolfOverlay(bool set) = 0; + + virtual void toggleDebugWindow() = 0; + + /// Cycle to next or previous spell + virtual void cycleSpell(bool next) = 0; + /// Cycle to next or previous weapon + virtual void cycleWeapon(bool next) = 0; }; } diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index 29f326a1b..9eb272e1b 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -3,29 +3,23 @@ #include #include +#include -#include +#include -#include "../mwworld/globals.hpp" #include "../mwworld/ptr.hpp" namespace Ogre { class Vector2; class Vector3; + class Quaternion; + class Image; } -namespace OEngine +namespace Loading { - namespace Render - { - class Fader; - } - - namespace Physic - { - class PhysicEngine; - } + class Listener; } namespace ESM @@ -38,13 +32,14 @@ namespace ESM struct Potion; struct Spell; struct NPC; - struct CellId; struct Armor; struct Weapon; struct Clothing; struct Enchantment; struct Book; struct EffectList; + struct CreatureLevList; + struct ItemLevList; } namespace MWRender @@ -95,6 +90,7 @@ namespace MWBase { std::string name; float x, y; // world position + ESM::CellId dest; }; World() {} @@ -107,15 +103,13 @@ namespace MWBase virtual void clear() = 0; virtual int countSavedGameRecords() const = 0; + virtual int countSavedGameCells() const = 0; virtual void write (ESM::ESMWriter& writer, Loading::Listener& listener) const = 0; - virtual void readRecord (ESM::ESMReader& reader, int32_t type, + virtual void readRecord (ESM::ESMReader& reader, uint32_t type, const std::map& contentFileMap) = 0; - virtual OEngine::Render::Fader* getFader() = 0; - ///< \todo remove this function. Rendering details should not be exposed. - virtual MWWorld::CellStore *getExterior (int x, int y) = 0; virtual MWWorld::CellStore *getInterior (const std::string& name) = 0; @@ -127,6 +121,7 @@ namespace MWBase virtual void setWaterHeight(const float height) = 0; virtual bool toggleWater() = 0; + virtual bool toggleWorld() = 0; virtual void adjustSky() = 0; @@ -144,20 +139,23 @@ namespace MWBase virtual MWWorld::LocalScripts& getLocalScripts() = 0; virtual bool hasCellChanged() const = 0; - ///< Has the player moved to a different cell, since the last frame? + ///< Has the set of active cells changed, since the last frame? virtual bool isCellExterior() const = 0; virtual bool isCellQuasiExterior() const = 0; virtual Ogre::Vector2 getNorthVector (MWWorld::CellStore* cell) = 0; - ///< get north vector (OGRE coordinates) for given interior cell + ///< get north vector for given interior cell virtual void getDoorMarkers (MWWorld::CellStore* cell, std::vector& out) = 0; ///< get a list of teleport door markers for a given cell, to be displayed on the local map - virtual void getInteriorMapPosition (Ogre::Vector2 position, float& nX, float& nY, int &x, int& y) = 0; - ///< see MWRender::LocalMap::getInteriorMapPosition + virtual void worldToInteriorMapPosition (Ogre::Vector2 position, float& nX, float& nY, int &x, int& y) = 0; + ///< see MWRender::LocalMap::worldToInteriorMapPosition + + virtual Ogre::Vector2 interiorMapToWorldPosition (float nX, float nY, int x, int y) = 0; + ///< see MWRender::LocalMap::interiorMapToWorldPosition virtual bool isPositionExplored (float nX, float nY, int x, int y, bool interior) = 0; ///< see MWRender::LocalMap::isPositionExplored @@ -203,6 +201,10 @@ namespace MWBase virtual MWWorld::Ptr searchPtrViaActorId (int actorId) = 0; ///< Search is limited to the active cells. + virtual MWWorld::Ptr findContainer (const MWWorld::Ptr& ptr) = 0; + ///< Return a pointer to a liveCellRef which contains \a ptr. + /// \note Search is limited to the active cells. + /// \todo enable reference in the OGRE scene virtual void enable (const MWWorld::Ptr& ptr) = 0; @@ -266,20 +268,25 @@ namespace MWBase virtual MWWorld::Ptr getFacedObject() = 0; ///< Return pointer to the object the player is looking at, if it is within activation range + virtual float getMaxActivationDistance() = 0; + /// Returns a pointer to the object the provided object would hit (if within the /// specified distance), and the point where the hit occurs. This will attempt to /// use the "Head" node, or alternatively the "Bip01 Head" node as a basis. virtual std::pair getHitContact(const MWWorld::Ptr &ptr, float distance) = 0; - virtual void adjustPosition (const MWWorld::Ptr& ptr) = 0; + virtual void adjustPosition (const MWWorld::Ptr& ptr, bool force) = 0; ///< Adjust position after load to be on ground. Must be called after model load. + /// @param force do this even if the ptr is flying virtual void fixPosition (const MWWorld::Ptr& actor) = 0; ///< Attempt to fix position so that the Ptr is no longer inside collision geometry. virtual void deleteObject (const MWWorld::Ptr& ptr) = 0; + virtual void undeleteObject (const MWWorld::Ptr& ptr) = 0; - virtual void moveObject (const MWWorld::Ptr& ptr, float x, float y, float z) = 0; + virtual MWWorld::Ptr moveObject (const MWWorld::Ptr& ptr, float x, float y, float z) = 0; + ///< @return an updated Ptr in case the Ptr's cell changes virtual void moveObject(const MWWorld::Ptr &ptr, MWWorld::CellStore* newCell, float x, float y, float z) = 0; @@ -356,6 +363,14 @@ namespace MWBase ///< Create a new record (of type book) in the ESM store. /// \return pointer to created record + virtual const ESM::CreatureLevList *createOverrideRecord (const ESM::CreatureLevList& record) = 0; + ///< Write this record to the ESM store, allowing it to override a pre-existing record with the same ID. + /// \return pointer to created record + + virtual const ESM::ItemLevList *createOverrideRecord (const ESM::ItemLevList& record) = 0; + ///< Write this record to the ESM store, allowing it to override a pre-existing record with the same ID. + /// \return pointer to created record + virtual void update (float duration, bool paused) = 0; virtual MWWorld::Ptr placeObject (const MWWorld::Ptr& object, float cursorX, float cursorY, int amount) = 0; @@ -374,17 +389,19 @@ namespace MWBase virtual bool canPlaceObject (float cursorX, float cursorY) = 0; ///< @return true if it is possible to place on object at specified cursor location - virtual void processChangedSettings (const Settings::CategorySettingVector& settings) = 0; + virtual void processChangedSettings (const std::set< std::pair >& settings) = 0; virtual bool isFlying(const MWWorld::Ptr &ptr) const = 0; virtual bool isSlowFalling(const MWWorld::Ptr &ptr) const = 0; virtual bool isSwimming(const MWWorld::Ptr &object) const = 0; + virtual bool isWading(const MWWorld::Ptr &object) const = 0; ///Is the head of the creature underwater? virtual bool isSubmerged(const MWWorld::Ptr &object) const = 0; virtual bool isUnderwater(const MWWorld::CellStore* cell, const Ogre::Vector3 &pos) const = 0; virtual bool isOnGround(const MWWorld::Ptr &ptr) const = 0; virtual void togglePOV() = 0; + virtual bool isFirstPerson() const = 0; virtual void togglePreviewMode(bool enable) = 0; virtual bool toggleVanityMode(bool enable) = 0; virtual void allowVanityMode(bool allow) = 0; @@ -400,10 +417,20 @@ namespace MWBase virtual void activateDoor(const MWWorld::Ptr& door) = 0; /// update movement state of a non-teleport door as specified /// @param state see MWClass::setDoorState + /// @note throws an exception when invoked on a teleport door virtual void activateDoor(const MWWorld::Ptr& door, int state) = 0; virtual bool getPlayerStandingOn (const MWWorld::Ptr& object) = 0; ///< @return true if the player is standing on \a object virtual bool getActorStandingOn (const MWWorld::Ptr& object) = 0; ///< @return true if any actor is standing on \a object + virtual bool getPlayerCollidingWith(const MWWorld::Ptr& object) = 0; ///< @return true if the player is colliding with \a object + virtual bool getActorCollidingWith (const MWWorld::Ptr& object) = 0; ///< @return true if any actor is colliding with \a object + virtual void hurtStandingActors (const MWWorld::Ptr& object, float dmgPerSecond) = 0; + ///< Apply a health difference to any actors standing on \a object. + /// To hurt actors, healthPerSecond should be a positive value. For a negative value, actors will be healed. + virtual void hurtCollidingActors (const MWWorld::Ptr& object, float dmgPerSecond) = 0; + ///< Apply a health difference to any actors colliding with \a object. + /// To hurt actors, healthPerSecond should be a positive value. For a negative value, actors will be healed. + virtual float getWindSpeed() = 0; virtual void getContainersOwnedBy (const MWWorld::Ptr& npc, std::vector& out) = 0; @@ -427,6 +454,7 @@ namespace MWBase /// \todo Probably shouldn't be here virtual MWRender::Animation* getAnimation(const MWWorld::Ptr &ptr) = 0; + virtual void reattachPlayerCamera() = 0; /// \todo this does not belong here virtual void frameStarted (float dt, bool paused) = 0; @@ -463,6 +491,9 @@ namespace MWBase virtual bool toggleGodMode() = 0; + virtual bool toggleScripts() = 0; + virtual bool getScriptsEnabled() const = 0; + /** * @brief startSpellCast attempt to start casting a spell. Might fail immediately if conditions are not met. * @param actor @@ -522,7 +553,7 @@ namespace MWBase virtual void spawnEffect (const std::string& model, const std::string& textureOverride, const Ogre::Vector3& worldPos) = 0; virtual void explodeSpell (const Ogre::Vector3& origin, const ESM::EffectList& effects, - const MWWorld::Ptr& caster, const std::string& id, const std::string& sourceName) = 0; + const MWWorld::Ptr& caster, ESM::RangeType rangeType, const std::string& id, const std::string& sourceName) = 0; virtual void activate (const MWWorld::Ptr& object, const MWWorld::Ptr& actor) = 0; @@ -531,6 +562,11 @@ namespace MWBase /// @see MWWorld::WeatherManager::getStormDirection virtual Ogre::Vector3 getStormDirection() const = 0; + + /// Resets all actors in the current active cells to their original location within that cell. + virtual void resetActors() = 0; + + virtual bool isWalkingOnWater (const MWWorld::Ptr& actor) = 0; }; } diff --git a/apps/openmw/mwclass/activator.cpp b/apps/openmw/mwclass/activator.cpp index 5bd144a73..457b0cec1 100644 --- a/apps/openmw/mwclass/activator.cpp +++ b/apps/openmw/mwclass/activator.cpp @@ -8,7 +8,8 @@ #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/world.hpp" -#include "../mwworld//cellstore.hpp" +#include "../mwworld/cellstore.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwworld/ptr.hpp" #include "../mwworld/physicssystem.hpp" #include "../mwworld/action.hpp" @@ -25,20 +26,23 @@ namespace MWClass { - void Activator::insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const + std::string Activator::getId (const MWWorld::Ptr& ptr) const + { + return ptr.get()->mBase->mId; + } + + void Activator::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { - const std::string model = getModel(ptr); if (!model.empty()) { MWRender::Actors& actors = renderingInterface.getActors(); - actors.insertActivator(ptr); + actors.insertActivator(ptr, model); } } - void Activator::insertObject(const MWWorld::Ptr& ptr, MWWorld::PhysicsSystem& physics) const + void Activator::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWWorld::PhysicsSystem& physics) const { - const std::string model = getModel(ptr); if(!model.empty()) - physics.addObject(ptr); + physics.addObject(ptr, model); MWBase::Environment::get().getMechanicsManager()->add(ptr); } diff --git a/apps/openmw/mwclass/activator.hpp b/apps/openmw/mwclass/activator.hpp index 1e772ef4f..e79318a55 100644 --- a/apps/openmw/mwclass/activator.hpp +++ b/apps/openmw/mwclass/activator.hpp @@ -13,10 +13,13 @@ namespace MWClass public: - virtual void insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const; + /// Return ID of \a ptr + virtual std::string getId (const MWWorld::Ptr& ptr) const; + + virtual void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const; ///< Add reference into a cell for rendering - virtual void insertObject(const MWWorld::Ptr& ptr, MWWorld::PhysicsSystem& physics) const; + virtual void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWWorld::PhysicsSystem& physics) const; virtual std::string getName (const MWWorld::Ptr& ptr) const; ///< \return name (the one that is to be presented to the user; not the internal one); diff --git a/apps/openmw/mwclass/apparatus.cpp b/apps/openmw/mwclass/apparatus.cpp index a162c3edd..316ba3ab6 100644 --- a/apps/openmw/mwclass/apparatus.cpp +++ b/apps/openmw/mwclass/apparatus.cpp @@ -21,19 +21,22 @@ namespace MWClass { - void Apparatus::insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const + std::string Apparatus::getId (const MWWorld::Ptr& ptr) const + { + return ptr.get()->mBase->mId; + } + + void Apparatus::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { - const std::string model = getModel(ptr); if (!model.empty()) { renderingInterface.getObjects().insertModel(ptr, model); } } - void Apparatus::insertObject(const MWWorld::Ptr& ptr, MWWorld::PhysicsSystem& physics) const + void Apparatus::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWWorld::PhysicsSystem& physics) const { - const std::string model = getModel(ptr); if(!model.empty()) - physics.addObject(ptr,true); + physics.addObject(ptr, model, true); } std::string Apparatus::getModel(const MWWorld::Ptr &ptr) const diff --git a/apps/openmw/mwclass/apparatus.hpp b/apps/openmw/mwclass/apparatus.hpp index 17b8b9254..2ab0a47e3 100644 --- a/apps/openmw/mwclass/apparatus.hpp +++ b/apps/openmw/mwclass/apparatus.hpp @@ -13,12 +13,15 @@ namespace MWClass public: + /// Return ID of \a ptr + virtual std::string getId (const MWWorld::Ptr& ptr) const; + virtual float getWeight (const MWWorld::Ptr& ptr) const; - virtual void insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const; + virtual void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const; ///< Add reference into a cell for rendering - virtual void insertObject(const MWWorld::Ptr& ptr, MWWorld::PhysicsSystem& physics) const; + virtual void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWWorld::PhysicsSystem& physics) const; virtual std::string getName (const MWWorld::Ptr& ptr) const; ///< \return name (the one that is to be presented to the user; not the internal one); diff --git a/apps/openmw/mwclass/armor.cpp b/apps/openmw/mwclass/armor.cpp index e102e7cf1..0f99d91da 100644 --- a/apps/openmw/mwclass/armor.cpp +++ b/apps/openmw/mwclass/armor.cpp @@ -14,6 +14,7 @@ #include "../mwworld/actionequip.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwworld/physicssystem.hpp" #include "../mwworld/nullaction.hpp" #include "../mwworld/containerstore.hpp" @@ -25,19 +26,22 @@ namespace MWClass { - void Armor::insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const + std::string Armor::getId (const MWWorld::Ptr& ptr) const + { + return ptr.get()->mBase->mId; + } + + void Armor::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { - const std::string model = getModel(ptr); if (!model.empty()) { renderingInterface.getObjects().insertModel(ptr, model); } } - void Armor::insertObject(const MWWorld::Ptr& ptr, MWWorld::PhysicsSystem& physics) const + void Armor::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWWorld::PhysicsSystem& physics) const { - const std::string model = getModel(ptr); if(!model.empty()) - physics.addObject(ptr,true); + physics.addObject(ptr, model, true); } std::string Armor::getModel(const MWWorld::Ptr &ptr) const @@ -152,15 +156,19 @@ namespace MWClass float iWeight = gmst.find (typeGmst)->getInt(); - if (iWeight * gmst.find ("fLightMaxMod")->getFloat()>= - ref->mBase->mData.mWeight) + float epsilon = 5e-4; + + if (ref->mBase->mData.mWeight == 0) + return ESM::Skill::Unarmored; + + if (ref->mBase->mData.mWeight <= iWeight * gmst.find ("fLightMaxMod")->getFloat() + epsilon) return ESM::Skill::LightArmor; - if (iWeight * gmst.find ("fMedMaxMod")->getFloat()>= - ref->mBase->mData.mWeight) + if (ref->mBase->mData.mWeight <= iWeight * gmst.find ("fMedMaxMod")->getFloat() + epsilon) return ESM::Skill::MediumArmor; - return ESM::Skill::HeavyArmor; + else + return ESM::Skill::HeavyArmor; } int Armor::getValue (const MWWorld::Ptr& ptr) const @@ -237,7 +245,8 @@ namespace MWClass else typeText = "#{sHeavy}"; - text += "\n#{sArmorRating}: " + MWGui::ToolTips::toString(ref->mBase->mData.mArmor); + text += "\n#{sArmorRating}: " + MWGui::ToolTips::toString(getEffectiveArmorRating(ptr, + MWBase::Environment::get().getWorld()->getPlayerPtr())); int remainingHealth = getItemHealth(ptr); text += "\n#{sCondition}: " + MWGui::ToolTips::toString(remainingHealth) + "/" @@ -282,6 +291,22 @@ namespace MWClass return record->mId; } + int Armor::getEffectiveArmorRating(const MWWorld::Ptr &ptr, const MWWorld::Ptr &actor) const + { + MWWorld::LiveCellRef *ref = ptr.get(); + + int armorSkillType = getEquipmentSkill(ptr); + int armorSkill = actor.getClass().getSkill(actor, armorSkillType); + + const MWBase::World *world = MWBase::Environment::get().getWorld(); + int iBaseArmorSkill = world->getStore().get().find("iBaseArmorSkill")->getInt(); + + if(ref->mBase->mData.mWeight == 0) + return ref->mBase->mData.mArmor; + else + return ref->mBase->mData.mArmor * armorSkill / iBaseArmorSkill; + } + std::pair Armor::canBeEquipped(const MWWorld::Ptr &ptr, const MWWorld::Ptr &npc) const { MWWorld::InventoryStore& invStore = npc.getClass().getInventoryStore(npc); @@ -371,7 +396,8 @@ namespace MWClass bool Armor::canSell (const MWWorld::Ptr& item, int npcServices) const { - return npcServices & ESM::NPC::Armor; + return (npcServices & ESM::NPC::Armor) + || ((npcServices & ESM::NPC::MagicItems) && !getEnchantment(item).empty()); } float Armor::getWeight(const MWWorld::Ptr &ptr) const diff --git a/apps/openmw/mwclass/armor.hpp b/apps/openmw/mwclass/armor.hpp index e9164f920..21d711a0d 100644 --- a/apps/openmw/mwclass/armor.hpp +++ b/apps/openmw/mwclass/armor.hpp @@ -12,12 +12,15 @@ namespace MWClass public: + /// Return ID of \a ptr + virtual std::string getId (const MWWorld::Ptr& ptr) const; + virtual float getWeight (const MWWorld::Ptr& ptr) const; - virtual void insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const; + virtual void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const; ///< Add reference into a cell for rendering - virtual void insertObject(const MWWorld::Ptr& ptr, MWWorld::PhysicsSystem& physics) const; + virtual void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWWorld::PhysicsSystem& physics) const; virtual std::string getName (const MWWorld::Ptr& ptr) const; ///< \return name (the one that is to be presented to the user; not the internal one); @@ -83,6 +86,9 @@ namespace MWClass virtual int getEnchantmentPoints (const MWWorld::Ptr& ptr) const; virtual bool canSell (const MWWorld::Ptr& item, int npcServices) const; + + /// Get the effective armor rating, factoring in the actor's skills, for the given armor. + virtual int getEffectiveArmorRating(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const; }; } diff --git a/apps/openmw/mwclass/book.cpp b/apps/openmw/mwclass/book.cpp index 824b69daf..a9c96e7c7 100644 --- a/apps/openmw/mwclass/book.cpp +++ b/apps/openmw/mwclass/book.cpp @@ -11,6 +11,7 @@ #include "../mwworld/actionread.hpp" #include "../mwworld/failedaction.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwworld/physicssystem.hpp" #include "../mwrender/objects.hpp" @@ -22,19 +23,22 @@ namespace MWClass { - void Book::insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const + std::string Book::getId (const MWWorld::Ptr& ptr) const + { + return ptr.get()->mBase->mId; + } + + void Book::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { - const std::string model = getModel(ptr); if (!model.empty()) { renderingInterface.getObjects().insertModel(ptr, model); } } - void Book::insertObject(const MWWorld::Ptr& ptr, MWWorld::PhysicsSystem& physics) const + void Book::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWWorld::PhysicsSystem& physics) const { - const std::string model = getModel(ptr); if(!model.empty()) - physics.addObject(ptr,true); + physics.addObject(ptr, model, true); } std::string Book::getModel(const MWWorld::Ptr &ptr) const @@ -197,7 +201,8 @@ namespace MWClass bool Book::canSell (const MWWorld::Ptr& item, int npcServices) const { - return npcServices & ESM::NPC::Books; + return (npcServices & ESM::NPC::Books) + || ((npcServices & ESM::NPC::MagicItems) && !getEnchantment(item).empty()); } float Book::getWeight(const MWWorld::Ptr &ptr) const diff --git a/apps/openmw/mwclass/book.hpp b/apps/openmw/mwclass/book.hpp index b60ef41d6..05ff88bb2 100644 --- a/apps/openmw/mwclass/book.hpp +++ b/apps/openmw/mwclass/book.hpp @@ -12,10 +12,13 @@ namespace MWClass public: - virtual void insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const; + /// Return ID of \a ptr + virtual std::string getId (const MWWorld::Ptr& ptr) const; + + virtual void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const; ///< Add reference into a cell for rendering - virtual void insertObject(const MWWorld::Ptr& ptr, MWWorld::PhysicsSystem& physics) const; + virtual void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWWorld::PhysicsSystem& physics) const; virtual std::string getName (const MWWorld::Ptr& ptr) const; ///< \return name (the one that is to be presented to the user; not the internal one); diff --git a/apps/openmw/mwclass/clothing.cpp b/apps/openmw/mwclass/clothing.cpp index bbe5f60bf..0fa686dda 100644 --- a/apps/openmw/mwclass/clothing.cpp +++ b/apps/openmw/mwclass/clothing.cpp @@ -12,6 +12,7 @@ #include "../mwworld/actionequip.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwworld/physicssystem.hpp" #include "../mwworld/nullaction.hpp" @@ -22,19 +23,22 @@ namespace MWClass { - void Clothing::insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const + std::string Clothing::getId (const MWWorld::Ptr& ptr) const + { + return ptr.get()->mBase->mId; + } + + void Clothing::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { - const std::string model = getModel(ptr); if (!model.empty()) { renderingInterface.getObjects().insertModel(ptr, model); } } - void Clothing::insertObject(const MWWorld::Ptr& ptr, MWWorld::PhysicsSystem& physics) const + void Clothing::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWWorld::PhysicsSystem& physics) const { - const std::string model = getModel(ptr); if(!model.empty()) - physics.addObject(ptr,true); + physics.addObject(ptr, model, true); } std::string Clothing::getModel(const MWWorld::Ptr &ptr) const @@ -287,7 +291,8 @@ namespace MWClass bool Clothing::canSell (const MWWorld::Ptr& item, int npcServices) const { - return npcServices & ESM::NPC::Clothing; + return (npcServices & ESM::NPC::Clothing) + || ((npcServices & ESM::NPC::MagicItems) && !getEnchantment(item).empty()); } float Clothing::getWeight(const MWWorld::Ptr &ptr) const diff --git a/apps/openmw/mwclass/clothing.hpp b/apps/openmw/mwclass/clothing.hpp index 052928238..570054348 100644 --- a/apps/openmw/mwclass/clothing.hpp +++ b/apps/openmw/mwclass/clothing.hpp @@ -12,10 +12,13 @@ namespace MWClass public: - virtual void insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const; + /// Return ID of \a ptr + virtual std::string getId (const MWWorld::Ptr& ptr) const; + + virtual void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const; ///< Add reference into a cell for rendering - virtual void insertObject(const MWWorld::Ptr& ptr, MWWorld::PhysicsSystem& physics) const; + virtual void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWWorld::PhysicsSystem& physics) const; virtual std::string getName (const MWWorld::Ptr& ptr) const; ///< \return name (the one that is to be presented to the user; not the internal one); diff --git a/apps/openmw/mwclass/container.cpp b/apps/openmw/mwclass/container.cpp index 362b97902..5f49a74b6 100644 --- a/apps/openmw/mwclass/container.cpp +++ b/apps/openmw/mwclass/container.cpp @@ -7,6 +7,7 @@ #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwbase/mechanicsmanager.hpp" #include "../mwworld/ptr.hpp" #include "../mwworld/failedaction.hpp" @@ -14,6 +15,7 @@ #include "../mwworld/containerstore.hpp" #include "../mwworld/customdata.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwworld/actionopen.hpp" #include "../mwworld/actiontrap.hpp" #include "../mwworld/physicssystem.hpp" @@ -21,7 +23,7 @@ #include "../mwgui/tooltips.hpp" -#include "../mwrender/objects.hpp" +#include "../mwrender/actors.hpp" #include "../mwrender/renderinginterface.hpp" #include "../mwmechanics/npcstats.hpp" @@ -43,6 +45,11 @@ namespace namespace MWClass { + std::string Container::getId (const MWWorld::Ptr& ptr) const + { + return ptr.get()->mBase->mId; + } + void Container::ensureCustomData (const MWWorld::Ptr& ptr) const { if (!ptr.getRefData().getCustomData()) @@ -52,8 +59,10 @@ namespace MWClass MWWorld::LiveCellRef *ref = ptr.get(); + // setting ownership not needed, since taking items from a container inherits the + // container's owner automatically data->mContainerStore.fill( - ref->mBase->mInventory, ptr.getCellRef().getOwner(), ptr.getCellRef().getFaction(), MWBase::Environment::get().getWorld()->getStore()); + ref->mBase->mInventory, ""); // store ptr.getRefData().setCustomData (data.release()); @@ -75,22 +84,25 @@ namespace MWClass MWWorld::LiveCellRef *ref = ptr.get(); const ESM::InventoryList& list = ref->mBase->mInventory; MWWorld::ContainerStore& store = getContainerStore(ptr); - store.restock(list, ptr, ptr.getCellRef().getOwner(), ptr.getCellRef().getFaction()); + + // setting ownership not needed, since taking items from a container inherits the + // container's owner automatically + store.restock(list, ptr, ""); } - void Container::insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const + void Container::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { - const std::string model = getModel(ptr); if (!model.empty()) { - renderingInterface.getObjects().insertModel(ptr, model); + MWRender::Actors& actors = renderingInterface.getActors(); + actors.insertActivator(ptr, model); } } - void Container::insertObject(const MWWorld::Ptr& ptr, MWWorld::PhysicsSystem& physics) const + void Container::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWWorld::PhysicsSystem& physics) const { - const std::string model = getModel(ptr); if(!model.empty()) - physics.addObject(ptr); + physics.addObject(ptr, model); + MWBase::Environment::get().getMechanicsManager()->add(ptr); } std::string Container::getModel(const MWWorld::Ptr &ptr) const diff --git a/apps/openmw/mwclass/container.hpp b/apps/openmw/mwclass/container.hpp index 9fc013e45..52873374e 100644 --- a/apps/openmw/mwclass/container.hpp +++ b/apps/openmw/mwclass/container.hpp @@ -15,10 +15,13 @@ namespace MWClass public: - virtual void insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const; + /// Return ID of \a ptr + virtual std::string getId (const MWWorld::Ptr& ptr) const; + + virtual void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const; ///< Add reference into a cell for rendering - virtual void insertObject(const MWWorld::Ptr& ptr, MWWorld::PhysicsSystem& physics) const; + virtual void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWWorld::PhysicsSystem& physics) const; virtual std::string getName (const MWWorld::Ptr& ptr) const; ///< \return name (the one that is to be presented to the user; not the internal one); diff --git a/apps/openmw/mwclass/creature.cpp b/apps/openmw/mwclass/creature.cpp index a74ee6fd6..e44f4ffc1 100644 --- a/apps/openmw/mwclass/creature.cpp +++ b/apps/openmw/mwclass/creature.cpp @@ -119,7 +119,12 @@ namespace MWClass // spells for (std::vector::const_iterator iter (ref->mBase->mSpells.mList.begin()); iter!=ref->mBase->mSpells.mList.end(); ++iter) - data->mCreatureStats.getSpells().add (*iter); + { + if (const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(*iter)) + data->mCreatureStats.getSpells().add (spell); + else /// \todo add option to make this a fatal error message pop-up, but default to warning for vanilla compatibility + std::cerr << "Warning: ignoring nonexistent spell '" << *iter << "' on creature '" << ref->mBase->mId << "'" << std::endl; + } // inventory if (ref->mBase->mFlags & ESM::Creature::Weapon) @@ -129,11 +134,12 @@ namespace MWClass data->mCreatureStats.setGoldPool(ref->mBase->mData.mGold); + data->mCreatureStats.setNeedRecalcDynamicStats(false); + // store ptr.getRefData().setCustomData(data.release()); - getContainerStore(ptr).fill(ref->mBase->mInventory, getId(ptr), "", - MWBase::Environment::get().getWorld()->getStore()); + getContainerStore(ptr).fill(ref->mBase->mInventory, getId(ptr)); if (ref->mBase->mFlags & ESM::Creature::Weapon) getInventoryStore(ptr).autoEquip(ptr); @@ -148,25 +154,24 @@ namespace MWClass return ref->mBase->mId; } - void Creature::adjustPosition(const MWWorld::Ptr& ptr) const + void Creature::adjustPosition(const MWWorld::Ptr& ptr, bool force) const { - MWBase::Environment::get().getWorld()->adjustPosition(ptr); + MWBase::Environment::get().getWorld()->adjustPosition(ptr, force); } - void Creature::insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const + void Creature::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { MWWorld::LiveCellRef *ref = ptr.get(); MWRender::Actors& actors = renderingInterface.getActors(); - actors.insertCreature(ptr, ref->mBase->mFlags & ESM::Creature::Weapon); + actors.insertCreature(ptr, model, ref->mBase->mFlags & ESM::Creature::Weapon); } - void Creature::insertObject(const MWWorld::Ptr& ptr, MWWorld::PhysicsSystem& physics) const + void Creature::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWWorld::PhysicsSystem& physics) const { - const std::string model = getModel(ptr); if(!model.empty()) { - physics.addActor(ptr); + physics.addActor(ptr, model); if (getCreatureStats(ptr).isDead()) MWBase::Environment::get().getWorld()->enableActorCollision(ptr, false); } @@ -209,6 +214,9 @@ namespace MWClass const MWWorld::Store &gmst = MWBase::Environment::get().getWorld()->getStore().get(); MWMechanics::CreatureStats &stats = getCreatureStats(ptr); + if (stats.getDrawState() != MWMechanics::DrawState_Weapon) + return; + // Get the weapon used (if hand-to-hand, weapon = inv.end()) MWWorld::Ptr weapon; if (ptr.getClass().hasInventoryStore(ptr)) @@ -219,18 +227,7 @@ namespace MWClass weapon = *weaponslot; } - // Reduce fatigue - // somewhat of a guess, but using the weapon weight makes sense - const float fFatigueAttackBase = gmst.find("fFatigueAttackBase")->getFloat(); - const float fFatigueAttackMult = gmst.find("fFatigueAttackMult")->getFloat(); - const float fWeaponFatigueMult = gmst.find("fWeaponFatigueMult")->getFloat(); - MWMechanics::DynamicStat fatigue = stats.getFatigue(); - const float normalizedEncumbrance = getEncumbrance(ptr) / getCapacity(ptr); - float fatigueLoss = fFatigueAttackBase + normalizedEncumbrance * fFatigueAttackMult; - if (!weapon.isEmpty()) - fatigueLoss += weapon.getClass().getWeight(weapon) * stats.getAttackStrength() * fWeaponFatigueMult; - fatigue.setCurrent(fatigue.getCurrent() - fatigueLoss); - stats.setFatigue(fatigue); + MWMechanics::applyFatigueLoss(ptr, weapon); // TODO: where is the distance defined? float dist = 200.f; @@ -255,23 +252,7 @@ namespace MWClass if((::rand()/(RAND_MAX+1.0)) > hitchance/100.0f) { victim.getClass().onHit(victim, 0.0f, false, MWWorld::Ptr(), ptr, false); - - // Weapon health is reduced by 1 even if the attack misses - const bool weaphashealth = !weapon.isEmpty() && weapon.getClass().hasItemHealth(weapon); - if(weaphashealth) - { - int weaphealth = weapon.getClass().getItemHealth(weapon); - - if (!MWBase::Environment::get().getWorld()->getGodModeState()) - { - weaphealth -= std::min(1, weaphealth); - weapon.getCellRef().setCharge(weaphealth); - } - - // Weapon broken? unequip it - if (weapon.getCellRef().getCharge() == 0) - weapon = *getInventoryStore(ptr).unequipItem(weapon, ptr); - } + MWMechanics::reduceWeaponCondition(0.f, false, weapon, ptr); return; } @@ -293,12 +274,10 @@ namespace MWClass break; } - // I think this should be random, since attack1-3 animations don't have an attack strength like NPCs do - float damage = min + (max - min) * ::rand()/(RAND_MAX+1.0); - + float damage = min + (max - min) * stats.getAttackStrength(); + bool healthdmg = true; if (!weapon.isEmpty()) { - const bool weaphashealth = weapon.getClass().hasItemHealth(weapon); const unsigned char *attack = NULL; if(type == ESM::Weapon::AT_Chop) attack = weapon.get()->mBase->mData.mChop; @@ -309,26 +288,10 @@ namespace MWClass if(attack) { damage = attack[0] + ((attack[1]-attack[0])*stats.getAttackStrength()); - damage *= 0.5f + (stats.getAttribute(ESM::Attribute::Luck).getModified() / 100.0f); - if(weaphashealth) - { - int weapmaxhealth = weapon.getClass().getItemMaxHealth(weapon); - int weaphealth = weapon.getClass().getItemHealth(weapon); - damage *= float(weaphealth) / weapmaxhealth; - - if (!MWBase::Environment::get().getWorld()->getGodModeState()) - { - // Reduce weapon charge by at least one, but cap at 0 - weaphealth -= std::min(std::max(1, - (int)(damage * gmst.find("fWeaponDamageMult")->getFloat())), weaphealth); - - weapon.getCellRef().setCharge(weaphealth); - } - - // Weapon broken? unequip it - if (weapon.getCellRef().getCharge() == 0) - weapon = *getInventoryStore(ptr).unequipItem(weapon, ptr); - } + damage *= gmst.find("fDamageStrengthBase")->getFloat() + + (stats.getAttribute(ESM::Attribute::Strength).getModified() * gmst.find("fDamageStrengthMult")->getFloat() * 0.1); + MWMechanics::adjustWeaponDamage(damage, weapon); + MWMechanics::reduceWeaponCondition(damage, true, weapon, ptr); } // Apply "On hit" enchanted weapons @@ -345,10 +308,14 @@ namespace MWClass } } } + else if (isBipedal(ptr)) + { + MWMechanics::getHandToHandDamage(ptr, victim, damage, healthdmg); + } MWMechanics::applyElementalShields(ptr, victim); - if (!weapon.isEmpty() && MWMechanics::blockMeleeAttack(ptr, victim, weapon, damage)) + if (MWMechanics::blockMeleeAttack(ptr, victim, weapon, damage)) damage = 0; if (damage > 0) @@ -356,25 +323,30 @@ namespace MWClass MWMechanics::diseaseContact(victim, ptr); - victim.getClass().onHit(victim, damage, true, weapon, ptr, true); + victim.getClass().onHit(victim, damage, healthdmg, weapon, ptr, true); } void Creature::onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, bool successful) const { // NOTE: 'object' and/or 'attacker' may be empty. - getCreatureStats(ptr).setAttacked(true); + if (!attacker.isEmpty() && !ptr.getClass().getCreatureStats(ptr).getAiSequence().isInCombat(attacker)) + getCreatureStats(ptr).setAttacked(true); // Self defense - if (!attacker.isEmpty() && !MWBase::Environment::get().getMechanicsManager()->isAggressive(ptr, attacker) - && (canWalk(ptr) || canFly(ptr) || canSwim(ptr))) // No retaliation for totally static creatures - // (they have no movement or attacks anyway) - MWBase::Environment::get().getMechanicsManager()->startCombat(ptr, attacker); + bool setOnPcHitMe = true; // Note OnPcHitMe is not set for friendly hits. + + // No retaliation for totally static creatures (they have no movement or attacks anyway) + if (isMobile(ptr) && !attacker.isEmpty()) + { + setOnPcHitMe = MWBase::Environment::get().getMechanicsManager()->actorAttacked(ptr, attacker); + } + + if(!object.isEmpty()) + getCreatureStats(ptr).setLastHitAttemptObject(object.getClass().getId(object)); if(!successful) { - // TODO: Handle HitAttemptOnMe script function - // Missed MWBase::Environment::get().getSoundManager()->playSound3D(ptr, "miss", 1.0f, 1.0f); return; @@ -383,7 +355,7 @@ namespace MWClass if(!object.isEmpty()) getCreatureStats(ptr).setLastHitObject(object.getClass().getId(object)); - if(!attacker.isEmpty() && attacker.getRefData().getHandle() == "player") + if(setOnPcHitMe && !attacker.isEmpty() && attacker.getRefData().getHandle() == "player") { const std::string &script = ptr.get()->mBase->mScript; /* Set the OnPCHitMe script variable. The script is responsible for clearing it. */ @@ -399,18 +371,21 @@ namespace MWClass if (damage > 0.f) { - // Check for knockdown - float agilityTerm = getCreatureStats(ptr).getAttribute(ESM::Attribute::Agility).getModified() * getGmst().fKnockDownMult->getFloat(); - float knockdownTerm = getCreatureStats(ptr).getAttribute(ESM::Attribute::Agility).getModified() - * getGmst().iKnockDownOddsMult->getInt() * 0.01 + getGmst().iKnockDownOddsBase->getInt(); - int roll = std::rand()/ (static_cast (RAND_MAX) + 1) * 100; // [0, 99] - if (ishealth && agilityTerm <= damage && knockdownTerm <= roll) + if (!attacker.isEmpty()) { - getCreatureStats(ptr).setKnockedDown(true); + // Check for knockdown + float agilityTerm = getCreatureStats(ptr).getAttribute(ESM::Attribute::Agility).getModified() * getGmst().fKnockDownMult->getFloat(); + float knockdownTerm = getCreatureStats(ptr).getAttribute(ESM::Attribute::Agility).getModified() + * getGmst().iKnockDownOddsMult->getInt() * 0.01 + getGmst().iKnockDownOddsBase->getInt(); + int roll = std::rand()/ (static_cast (RAND_MAX) + 1) * 100; // [0, 99] + if (ishealth && agilityTerm <= damage && knockdownTerm <= roll) + { + getCreatureStats(ptr).setKnockedDown(true); + } + else + getCreatureStats(ptr).setHitRecovery(true); // Is this supposed to always occur? } - else - getCreatureStats(ptr).setHitRecovery(true); // Is this supposed to always occur? damage = std::max(1.f, damage); @@ -492,6 +467,8 @@ namespace MWClass if(getCreatureStats(ptr).isDead()) return boost::shared_ptr(new MWWorld::ActionOpen(ptr, true)); + if(ptr.getClass().getCreatureStats(ptr).getAiSequence().isInCombat()) + return boost::shared_ptr(new MWWorld::FailedAction("")); return boost::shared_ptr(new MWWorld::ActionTalk(ptr)); } @@ -543,9 +520,7 @@ namespace MWClass bool Creature::hasToolTip (const MWWorld::Ptr& ptr) const { - /// \todo We don't want tooltips for Creatures in combat mode. - - return true; + return !ptr.getClass().getCreatureStats(ptr).getAiSequence().isInCombat() || getCreatureStats(ptr).isDead(); } float Creature::getSpeed(const MWWorld::Ptr &ptr) const @@ -559,22 +534,22 @@ namespace MWClass const MWBase::World *world = MWBase::Environment::get().getWorld(); const MWMechanics::MagicEffects &mageffects = stats.getMagicEffects(); - const float normalizedEncumbrance = getEncumbrance(ptr) / getCapacity(ptr); - bool running = ptr.getClass().getCreatureStats(ptr).getStance(MWMechanics::CreatureStats::Stance_Run); // The Run speed difference for creatures comes from the animation speed difference (see runStateToWalkState in character.cpp) float runSpeed = walkSpeed; float moveSpeed; - if(normalizedEncumbrance >= 1.0f) + + if(getEncumbrance(ptr) > getCapacity(ptr)) moveSpeed = 0.0f; - else if(canFly(ptr) || (mageffects.get(ESM::MagicEffect::Levitate).mMagnitude > 0 && + else if(canFly(ptr) || (mageffects.get(ESM::MagicEffect::Levitate).getMagnitude() > 0 && world->isLevitationEnabled())) { float flySpeed = 0.01f*(stats.getAttribute(ESM::Attribute::Speed).getModified() + - mageffects.get(ESM::MagicEffect::Levitate).mMagnitude); + mageffects.get(ESM::MagicEffect::Levitate).getMagnitude()); flySpeed = gmst.fMinFlySpeed->getFloat() + flySpeed*(gmst.fMaxFlySpeed->getFloat() - gmst.fMinFlySpeed->getFloat()); + const float normalizedEncumbrance = getNormalizedEncumbrance(ptr); flySpeed *= 1.0f - gmst.fEncumberedMoveEffect->getFloat() * normalizedEncumbrance; flySpeed = std::max(0.0f, flySpeed); moveSpeed = flySpeed; @@ -584,7 +559,7 @@ namespace MWClass float swimSpeed = walkSpeed; if(running) swimSpeed = runSpeed; - swimSpeed *= 1.0f + 0.01f * mageffects.get(ESM::MagicEffect::SwiftSwim).mMagnitude; + swimSpeed *= 1.0f + 0.01f * mageffects.get(ESM::MagicEffect::SwiftSwim).getMagnitude(); swimSpeed *= gmst.fSwimRunBase->getFloat() + 0.01f*getSkill(ptr, ESM::Skill::Athletics) * gmst.fSwimRunAthleticsMult->getFloat(); moveSpeed = swimSpeed; @@ -645,7 +620,7 @@ namespace MWClass float Creature::getArmorRating (const MWWorld::Ptr& ptr) const { // Note this is currently unused. Creatures do not use armor mitigation. - return getCreatureStats(ptr).getMagicEffects().get(ESM::MagicEffect::Shield).mMagnitude; + return getCreatureStats(ptr).getMagicEffects().get(ESM::MagicEffect::Shield).getMagnitude(); } float Creature::getCapacity (const MWWorld::Ptr& ptr) const @@ -660,9 +635,9 @@ namespace MWClass const MWMechanics::CreatureStats& stats = getCreatureStats (ptr); - weight -= stats.getMagicEffects().get (MWMechanics::EffectKey (ESM::MagicEffect::Feather)).mMagnitude; + weight -= stats.getMagicEffects().get (MWMechanics::EffectKey (ESM::MagicEffect::Feather)).getMagnitude(); - weight += stats.getMagicEffects().get (MWMechanics::EffectKey (ESM::MagicEffect::Burden)).mMagnitude; + weight += stats.getMagicEffects().get (MWMechanics::EffectKey (ESM::MagicEffect::Burden)).getMagnitude(); if (weight<0) weight = 0; @@ -694,15 +669,15 @@ namespace MWClass if(type >= 0) { std::vector sounds; - sounds.reserve(8); - std::string ptrid = Creature::getId(ptr); + MWWorld::LiveCellRef* ref = ptr.get(); + + const std::string& ourId = (ref->mBase->mOriginal.empty()) ? getId(ptr) : ref->mBase->mOriginal; + MWWorld::Store::iterator sound = store.begin(); while(sound != store.end()) { - if(type == sound->mType && !sound->mCreature.empty() && - Misc::StringUtils::ciEqual(ptrid.substr(0, sound->mCreature.size()), - sound->mCreature)) + if (type == sound->mType && !sound->mCreature.empty() && (Misc::StringUtils::ciEqual(ourId, sound->mCreature))) sounds.push_back(&*sound); ++sound; } @@ -710,6 +685,9 @@ namespace MWClass return sounds[(int)(rand()/(RAND_MAX+1.0)*sounds.size())]->mSound; } + if (type == ESM::SoundGenerator::Land) + return "Body Fall Large"; + return ""; } @@ -760,7 +738,7 @@ namespace MWClass { MWBase::World *world = MWBase::Environment::get().getWorld(); Ogre::Vector3 pos(ptr.getRefData().getPosition().pos); - if(world->isUnderwater(ptr.getCell(), pos)) + if(world->isUnderwater(ptr.getCell(), pos) || world->isWalkingOnWater(ptr)) return 2; if(world->isOnGround(ptr)) return 0; @@ -770,7 +748,7 @@ namespace MWClass { MWBase::World *world = MWBase::Environment::get().getWorld(); Ogre::Vector3 pos(ptr.getRefData().getPosition().pos); - if(world->isUnderwater(ptr.getCell(), pos)) + if(world->isUnderwater(ptr.getCell(), pos) || world->isWalkingOnWater(ptr)) return 3; if(world->isOnGround(ptr)) return 1; @@ -826,6 +804,9 @@ namespace MWClass void Creature::readAdditionalState (const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const { + if (!state.mHasCustomState) + return; + const ESM::CreatureState& state2 = dynamic_cast (state); ensureCustomData(ptr); @@ -854,7 +835,6 @@ namespace MWClass customData.mContainerStore->readState (state2.mInventory); customData.mCreatureStats.readState (state2.mCreatureStats); - } void Creature::writeAdditionalState (const MWWorld::Ptr& ptr, ESM::ObjectState& state) @@ -862,6 +842,12 @@ namespace MWClass { ESM::CreatureState& state2 = dynamic_cast (state); + if (!ptr.getRefData().getCustomData()) + { + state.mHasCustomState = false; + return; + } + ensureCustomData (ptr); CreatureCustomData& customData = dynamic_cast (*ptr.getRefData().getCustomData()); @@ -881,7 +867,7 @@ namespace MWClass { // Note we do not respawn moved references in the cell they were moved to. Instead they are respawned in the original cell. // This also means we cannot respawn dynamically placed references with no content file connection. - if (ptr.getCellRef().getRefNum().mContentFile != -1) + if (ptr.getCellRef().hasContentFile()) { if (ptr.getRefData().getCount() == 0) ptr.getRefData().setCount(1); @@ -899,6 +885,18 @@ namespace MWClass MWWorld::LiveCellRef *ref = ptr.get(); const ESM::InventoryList& list = ref->mBase->mInventory; MWWorld::ContainerStore& store = getContainerStore(ptr); - store.restock(list, ptr, ptr.getCellRef().getRefId(), ptr.getCellRef().getFaction()); + store.restock(list, ptr, ptr.getCellRef().getRefId()); + } + + int Creature::getBaseFightRating(const MWWorld::Ptr &ptr) const + { + MWWorld::LiveCellRef *ref = ptr.get(); + return ref->mBase->mAiData.mFight; + } + + void Creature::adjustScale(const MWWorld::Ptr &ptr, float &scale) const + { + MWWorld::LiveCellRef *ref = ptr.get(); + scale *= ref->mBase->mScale; } } diff --git a/apps/openmw/mwclass/creature.hpp b/apps/openmw/mwclass/creature.hpp index 6920a4b1d..e11529b2e 100644 --- a/apps/openmw/mwclass/creature.hpp +++ b/apps/openmw/mwclass/creature.hpp @@ -44,12 +44,14 @@ namespace MWClass virtual std::string getId (const MWWorld::Ptr& ptr) const; ///< Return ID of \a ptr - virtual void insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const; + virtual void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const; ///< Add reference into a cell for rendering - virtual void insertObject(const MWWorld::Ptr& ptr, MWWorld::PhysicsSystem& physics) const; + virtual void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWWorld::PhysicsSystem& physics) const; - virtual void adjustPosition(const MWWorld::Ptr& ptr) const; + virtual void adjustPosition(const MWWorld::Ptr& ptr, bool force) const; + ///< Adjust position to stand on ground. Must be called post model load + /// @param force do this even if the ptr is flying virtual std::string getName (const MWWorld::Ptr& ptr) const; ///< \return name (the one that is to be presented to the user; not the internal one); @@ -152,6 +154,10 @@ namespace MWClass virtual void respawn (const MWWorld::Ptr& ptr) const; virtual void restock (const MWWorld::Ptr &ptr) const; + + virtual int getBaseFightRating(const MWWorld::Ptr &ptr) const; + + virtual void adjustScale(const MWWorld::Ptr& ptr,float& scale) const; }; } diff --git a/apps/openmw/mwclass/creaturelevlist.cpp b/apps/openmw/mwclass/creaturelevlist.cpp index 784304804..dbc4b6af7 100644 --- a/apps/openmw/mwclass/creaturelevlist.cpp +++ b/apps/openmw/mwclass/creaturelevlist.cpp @@ -27,6 +27,11 @@ namespace namespace MWClass { + std::string CreatureLevList::getId (const MWWorld::Ptr& ptr) const + { + return ptr.get()->mBase->mId; + } + std::string CreatureLevList::getName (const MWWorld::Ptr& ptr) const { return ""; @@ -47,7 +52,7 @@ namespace MWClass registerClass (typeid (ESM::CreatureLevList).name(), instance); } - void CreatureLevList::insertObjectRendering(const MWWorld::Ptr &ptr, MWRender::RenderingInterface &renderingInterface) const + void CreatureLevList::insertObjectRendering(const MWWorld::Ptr &ptr, const std::string& model, MWRender::RenderingInterface &renderingInterface) const { ensureCustomData(ptr); @@ -78,6 +83,8 @@ namespace MWClass customData.mSpawnActorId = placed.getClass().getCreatureStats(placed).getActorId(); customData.mSpawn = false; } + else + customData.mSpawn = false; } void CreatureLevList::ensureCustomData(const MWWorld::Ptr &ptr) const diff --git a/apps/openmw/mwclass/creaturelevlist.hpp b/apps/openmw/mwclass/creaturelevlist.hpp index 6c51a3189..177aa7235 100644 --- a/apps/openmw/mwclass/creaturelevlist.hpp +++ b/apps/openmw/mwclass/creaturelevlist.hpp @@ -11,13 +11,16 @@ namespace MWClass public: + /// Return ID of \a ptr + virtual std::string getId (const MWWorld::Ptr& ptr) const; + virtual std::string getName (const MWWorld::Ptr& ptr) const; ///< \return name (the one that is to be presented to the user; not the internal one); /// can return an empty string. static void registerSelf(); - virtual void insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const; + virtual void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const; ///< Add reference into a cell for rendering virtual void readAdditionalState (const MWWorld::Ptr& ptr, const ESM::ObjectState& state) diff --git a/apps/openmw/mwclass/door.cpp b/apps/openmw/mwclass/door.cpp index d0ba37f0b..10b9b437d 100644 --- a/apps/openmw/mwclass/door.cpp +++ b/apps/openmw/mwclass/door.cpp @@ -8,6 +8,7 @@ #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/soundmanager.hpp" +#include "../mwbase/mechanicsmanager.hpp" #include "../mwworld/ptr.hpp" #include "../mwworld/nullaction.hpp" @@ -15,6 +16,7 @@ #include "../mwworld/actionteleport.hpp" #include "../mwworld/actiondoor.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwworld/physicssystem.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/actiontrap.hpp" @@ -22,7 +24,7 @@ #include "../mwgui/tooltips.hpp" -#include "../mwrender/objects.hpp" +#include "../mwrender/actors.hpp" #include "../mwrender/renderinginterface.hpp" namespace @@ -42,19 +44,23 @@ namespace namespace MWClass { - void Door::insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const + std::string Door::getId (const MWWorld::Ptr& ptr) const + { + return ptr.get()->mBase->mId; + } + + void Door::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { - const std::string model = getModel(ptr); if (!model.empty()) { - renderingInterface.getObjects().insertModel(ptr, model); + MWRender::Actors& actors = renderingInterface.getActors(); + actors.insertActivator(ptr, model); } } - void Door::insertObject(const MWWorld::Ptr& ptr, MWWorld::PhysicsSystem& physics) const + void Door::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWWorld::PhysicsSystem& physics) const { - const std::string model = getModel(ptr); if(!model.empty()) - physics.addObject(ptr); + physics.addObject(ptr, model); // Resume the door's opening/closing animation if it wasn't finished if (ptr.getRefData().getCustomData()) @@ -65,6 +71,8 @@ namespace MWClass MWBase::Environment::get().getWorld()->activateDoor(ptr, customData.mDoorState); } } + + MWBase::Environment::get().getMechanicsManager()->add(ptr); } std::string Door::getModel(const MWWorld::Ptr &ptr) const @@ -85,9 +93,6 @@ namespace MWClass MWWorld::LiveCellRef *ref = ptr.get(); - if (ptr.getCellRef().getTeleport() && !ptr.getCellRef().getDestCell().empty()) // TODO doors that lead to exteriors - return ptr.getCellRef().getDestCell(); - return ref->mBase->mName; } @@ -319,6 +324,9 @@ namespace MWClass void Door::setDoorState (const MWWorld::Ptr &ptr, int state) const { + if (ptr.getCellRef().getTeleport()) + throw std::runtime_error("load doors can't be moved"); + ensureCustomData(ptr); DoorCustomData& customData = dynamic_cast(*ptr.getRefData().getCustomData()); customData.mDoorState = state; diff --git a/apps/openmw/mwclass/door.hpp b/apps/openmw/mwclass/door.hpp index 12b360aa8..c5f258d3e 100644 --- a/apps/openmw/mwclass/door.hpp +++ b/apps/openmw/mwclass/door.hpp @@ -16,10 +16,13 @@ namespace MWClass public: - virtual void insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const; + /// Return ID of \a ptr + virtual std::string getId (const MWWorld::Ptr& ptr) const; + + virtual void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const; ///< Add reference into a cell for rendering - virtual void insertObject(const MWWorld::Ptr& ptr, MWWorld::PhysicsSystem& physics) const; + virtual void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWWorld::PhysicsSystem& physics) const; virtual std::string getName (const MWWorld::Ptr& ptr) const; ///< \return name (the one that is to be presented to the user; not the internal one); diff --git a/apps/openmw/mwclass/ingredient.cpp b/apps/openmw/mwclass/ingredient.cpp index fa03f23ff..9f662a60e 100644 --- a/apps/openmw/mwclass/ingredient.cpp +++ b/apps/openmw/mwclass/ingredient.cpp @@ -10,6 +10,7 @@ #include "../mwworld/ptr.hpp" #include "../mwworld/actiontake.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwworld/physicssystem.hpp" #include "../mwworld/actioneat.hpp" #include "../mwworld/nullaction.hpp" @@ -31,19 +32,17 @@ namespace MWClass return ref->mBase->mId; } - void Ingredient::insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const + void Ingredient::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { - const std::string model = getModel(ptr); if (!model.empty()) { renderingInterface.getObjects().insertModel(ptr, model); } } - void Ingredient::insertObject(const MWWorld::Ptr& ptr, MWWorld::PhysicsSystem& physics) const + void Ingredient::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWWorld::PhysicsSystem& physics) const { - const std::string model = getModel(ptr); if(!model.empty()) - physics.addObject(ptr,true); + physics.addObject(ptr, model, true); } std::string Ingredient::getModel(const MWWorld::Ptr &ptr) const diff --git a/apps/openmw/mwclass/ingredient.hpp b/apps/openmw/mwclass/ingredient.hpp index 690dd601a..a4681f462 100644 --- a/apps/openmw/mwclass/ingredient.hpp +++ b/apps/openmw/mwclass/ingredient.hpp @@ -15,10 +15,10 @@ namespace MWClass virtual std::string getId (const MWWorld::Ptr& ptr) const; ///< Return ID of \a ptr - virtual void insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const; + virtual void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const; ///< Add reference into a cell for rendering - virtual void insertObject(const MWWorld::Ptr& ptr, MWWorld::PhysicsSystem& physics) const; + virtual void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWWorld::PhysicsSystem& physics) const; virtual std::string getName (const MWWorld::Ptr& ptr) const; ///< \return name (the one that is to be presented to the user; not the internal one); diff --git a/apps/openmw/mwclass/itemlevlist.cpp b/apps/openmw/mwclass/itemlevlist.cpp index 6ed9ab2e5..d31080bb2 100644 --- a/apps/openmw/mwclass/itemlevlist.cpp +++ b/apps/openmw/mwclass/itemlevlist.cpp @@ -5,6 +5,11 @@ namespace MWClass { + std::string ItemLevList::getId (const MWWorld::Ptr& ptr) const + { + return ptr.get()->mBase->mId; + } + std::string ItemLevList::getName (const MWWorld::Ptr& ptr) const { return ""; diff --git a/apps/openmw/mwclass/itemlevlist.hpp b/apps/openmw/mwclass/itemlevlist.hpp index 0b71b072c..2b507135f 100644 --- a/apps/openmw/mwclass/itemlevlist.hpp +++ b/apps/openmw/mwclass/itemlevlist.hpp @@ -9,6 +9,9 @@ namespace MWClass { public: + /// Return ID of \a ptr + virtual std::string getId (const MWWorld::Ptr& ptr) const; + virtual std::string getName (const MWWorld::Ptr& ptr) const; ///< \return name (the one that is to be presented to the user; not the internal one); /// can return an empty string. diff --git a/apps/openmw/mwclass/light.cpp b/apps/openmw/mwclass/light.cpp index 8237b07f2..35a21b63e 100644 --- a/apps/openmw/mwclass/light.cpp +++ b/apps/openmw/mwclass/light.cpp @@ -2,12 +2,13 @@ #include "light.hpp" #include -#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwbase/mechanicsmanager.hpp" #include "../mwworld/ptr.hpp" #include "../mwworld/actiontake.hpp" @@ -22,54 +23,41 @@ #include "../mwgui/tooltips.hpp" #include "../mwrender/objects.hpp" +#include "../mwrender/actors.hpp" #include "../mwrender/renderinginterface.hpp" -namespace +namespace MWClass { - struct LightCustomData : public MWWorld::CustomData + std::string Light::getId (const MWWorld::Ptr& ptr) const { - float mTime; - ///< Time remaining - - LightCustomData(MWWorld::Ptr ptr) - { - MWWorld::LiveCellRef *ref = ptr.get(); - mTime = ref->mBase->mData.mTime; - } - ///< Constructs this CustomData from the base values for Ptr. - - virtual MWWorld::CustomData *clone() const - { - return new LightCustomData (*this); - } - }; -} + return ptr.get()->mBase->mId; + } -namespace MWClass -{ - void Light::insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const + void Light::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { - const std::string model = getModel(ptr); + MWWorld::LiveCellRef *ref = + ptr.get(); // Insert even if model is empty, so that the light is added - renderingInterface.getObjects().insertModel(ptr, model); + MWRender::Actors& actors = renderingInterface.getActors(); + actors.insertActivator(ptr, model, !(ref->mBase->mData.mFlags & ESM::Light::OffDefault)); } - void Light::insertObject(const MWWorld::Ptr& ptr, MWWorld::PhysicsSystem& physics) const + void Light::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWWorld::PhysicsSystem& physics) const { MWWorld::LiveCellRef *ref = ptr.get(); assert (ref->mBase != NULL); - const std::string &model = ref->mBase->mModel; - if(!model.empty()) - physics.addObject(ptr,ref->mBase->mData.mFlags & ESM::Light::Carry); + physics.addObject(ptr, model, ref->mBase->mData.mFlags & ESM::Light::Carry); - if (!ref->mBase->mSound.empty()) + if (!ref->mBase->mSound.empty() && !(ref->mBase->mData.mFlags & ESM::Light::OffDefault)) MWBase::Environment::get().getSoundManager()->playSound3D(ptr, ref->mBase->mSound, 1.0, 1.0, MWBase::SoundManager::Play_TypeSfx, MWBase::SoundManager::Play_Loop); + + MWBase::Environment::get().getMechanicsManager()->add(ptr); } std::string Light::getModel(const MWWorld::Ptr &ptr) const @@ -183,8 +171,11 @@ namespace MWClass std::string text; - text += "\n#{sWeight}: " + MWGui::ToolTips::toString(ref->mBase->mData.mWeight); - text += MWGui::ToolTips::getValueString(ref->mBase->mData.mValue, "#{sValue}"); + if (ref->mBase->mData.mWeight != 0) + { + text += "\n#{sWeight}: " + MWGui::ToolTips::toString(ref->mBase->mData.mWeight); + text += MWGui::ToolTips::getValueString(ref->mBase->mData.mValue, "#{sValue}"); + } if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); @@ -207,17 +198,16 @@ namespace MWClass void Light::setRemainingUsageTime (const MWWorld::Ptr& ptr, float duration) const { - ensureCustomData(ptr); - - float &timeRemaining = dynamic_cast (*ptr.getRefData().getCustomData()).mTime; - timeRemaining = duration; + ptr.getCellRef().setChargeFloat(duration); } float Light::getRemainingUsageTime (const MWWorld::Ptr& ptr) const { - ensureCustomData(ptr); - - return dynamic_cast (*ptr.getRefData().getCustomData()).mTime; + MWWorld::LiveCellRef *ref = ptr.get(); + if (ptr.getCellRef().getCharge() == -1) + return ref->mBase->mData.mTime; + else + return ptr.getCellRef().getChargeFloat(); } MWWorld::Ptr @@ -229,12 +219,6 @@ namespace MWClass return MWWorld::Ptr(&cell.get().insert(*ref), &cell); } - void Light::ensureCustomData (const MWWorld::Ptr& ptr) const - { - if (!ptr.getRefData().getCustomData()) - ptr.getRefData().setCustomData(new LightCustomData(ptr)); - } - bool Light::canSell (const MWWorld::Ptr& item, int npcServices) const { return npcServices & ESM::NPC::Lights; @@ -270,23 +254,8 @@ namespace MWClass return std::make_pair(1,""); } - void Light::readAdditionalState (const MWWorld::Ptr& ptr, const ESM::ObjectState& state) - const + std::string Light::getSound(const MWWorld::Ptr& ptr) const { - const ESM::LightState& state2 = dynamic_cast (state); - - ensureCustomData (ptr); - - dynamic_cast (*ptr.getRefData().getCustomData()).mTime = state2.mTime; - } - - void Light::writeAdditionalState (const MWWorld::Ptr& ptr, ESM::ObjectState& state) - const - { - ESM::LightState& state2 = dynamic_cast (state); - - ensureCustomData (ptr); - - state2.mTime = dynamic_cast (*ptr.getRefData().getCustomData()).mTime; + return ptr.get()->mBase->mSound; } } diff --git a/apps/openmw/mwclass/light.hpp b/apps/openmw/mwclass/light.hpp index 5568e1727..8658375b7 100644 --- a/apps/openmw/mwclass/light.hpp +++ b/apps/openmw/mwclass/light.hpp @@ -10,14 +10,15 @@ namespace MWClass virtual MWWorld::Ptr copyToCellImpl(const MWWorld::Ptr &ptr, MWWorld::CellStore &cell) const; - void ensureCustomData (const MWWorld::Ptr& ptr) const; - public: - virtual void insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const; + /// Return ID of \a ptr + virtual std::string getId (const MWWorld::Ptr& ptr) const; + + virtual void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const; ///< Add reference into a cell for rendering - virtual void insertObject(const MWWorld::Ptr& ptr, MWWorld::PhysicsSystem& physics) const; + virtual void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWWorld::PhysicsSystem& physics) const; virtual std::string getName (const MWWorld::Ptr& ptr) const; ///< \return name (the one that is to be presented to the user; not the internal one); @@ -72,13 +73,7 @@ namespace MWClass std::pair canBeEquipped(const MWWorld::Ptr &ptr, const MWWorld::Ptr &npc) const; - virtual void readAdditionalState (const MWWorld::Ptr& ptr, const ESM::ObjectState& state) - const; - ///< Read additional state from \a state into \a ptr. - - virtual void writeAdditionalState (const MWWorld::Ptr& ptr, ESM::ObjectState& state) - const; - ///< Write additional state from \a ptr into \a state. + virtual std::string getSound(const MWWorld::Ptr& ptr) const; }; } diff --git a/apps/openmw/mwclass/lockpick.cpp b/apps/openmw/mwclass/lockpick.cpp index b0129f403..e78c43eee 100644 --- a/apps/openmw/mwclass/lockpick.cpp +++ b/apps/openmw/mwclass/lockpick.cpp @@ -22,19 +22,22 @@ namespace MWClass { - void Lockpick::insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const + std::string Lockpick::getId (const MWWorld::Ptr& ptr) const + { + return ptr.get()->mBase->mId; + } + + void Lockpick::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { - const std::string model = getModel(ptr); if (!model.empty()) { renderingInterface.getObjects().insertModel(ptr, model); } } - void Lockpick::insertObject(const MWWorld::Ptr& ptr, MWWorld::PhysicsSystem& physics) const + void Lockpick::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWWorld::PhysicsSystem& physics) const { - const std::string model = getModel(ptr); if(!model.empty()) - physics.addObject(ptr,true); + physics.addObject(ptr, model, true); } std::string Lockpick::getModel(const MWWorld::Ptr &ptr) const diff --git a/apps/openmw/mwclass/lockpick.hpp b/apps/openmw/mwclass/lockpick.hpp index 8f5a699d9..293a40be1 100644 --- a/apps/openmw/mwclass/lockpick.hpp +++ b/apps/openmw/mwclass/lockpick.hpp @@ -12,10 +12,13 @@ namespace MWClass public: - virtual void insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const; + /// Return ID of \a ptr + virtual std::string getId (const MWWorld::Ptr& ptr) const; + + virtual void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const; ///< Add reference into a cell for rendering - virtual void insertObject(const MWWorld::Ptr& ptr, MWWorld::PhysicsSystem& physics) const; + virtual void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWWorld::PhysicsSystem& physics) const; virtual std::string getName (const MWWorld::Ptr& ptr) const; ///< \return name (the one that is to be presented to the user; not the internal one); diff --git a/apps/openmw/mwclass/misc.cpp b/apps/openmw/mwclass/misc.cpp index 74b10ee3d..f9cfd8e0b 100644 --- a/apps/openmw/mwclass/misc.cpp +++ b/apps/openmw/mwclass/misc.cpp @@ -12,6 +12,7 @@ #include "../mwworld/ptr.hpp" #include "../mwworld/actiontake.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwworld/physicssystem.hpp" #include "../mwworld/manualref.hpp" #include "../mwworld/nullaction.hpp" @@ -38,19 +39,22 @@ bool isGold (const MWWorld::Ptr& ptr) namespace MWClass { - void Miscellaneous::insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const + std::string Miscellaneous::getId (const MWWorld::Ptr& ptr) const + { + return ptr.get()->mBase->mId; + } + + void Miscellaneous::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { - const std::string model = getModel(ptr); if (!model.empty()) { renderingInterface.getObjects().insertModel(ptr, model); } } - void Miscellaneous::insertObject(const MWWorld::Ptr& ptr, MWWorld::PhysicsSystem& physics) const + void Miscellaneous::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWWorld::PhysicsSystem& physics) const { - const std::string model = getModel(ptr); if(!model.empty()) - physics.addObject(ptr,true); + physics.addObject(ptr, model, true); } std::string Miscellaneous::getModel(const MWWorld::Ptr &ptr) const @@ -175,7 +179,7 @@ namespace MWClass std::string text; - if (!gold) + if (!gold && !ref->mBase->mData.mIsKey) { text += "\n#{sWeight}: " + MWGui::ToolTips::toString(ref->mBase->mData.mWeight); text += MWGui::ToolTips::getValueString(getValue(ptr), "#{sValue}"); diff --git a/apps/openmw/mwclass/misc.hpp b/apps/openmw/mwclass/misc.hpp index 16e9ca10b..23160d41c 100644 --- a/apps/openmw/mwclass/misc.hpp +++ b/apps/openmw/mwclass/misc.hpp @@ -12,10 +12,13 @@ namespace MWClass public: - virtual void insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const; + /// Return ID of \a ptr + virtual std::string getId (const MWWorld::Ptr& ptr) const; + + virtual void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const; ///< Add reference into a cell for rendering - virtual void insertObject(const MWWorld::Ptr& ptr, MWWorld::PhysicsSystem& physics) const; + virtual void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWWorld::PhysicsSystem& physics) const; virtual std::string getName (const MWWorld::Ptr& ptr) const; ///< \return name (the one that is to be presented to the user; not the internal one); diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index 49e016d4f..52e5a0a95 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -24,6 +24,7 @@ #include "../mwmechanics/combat.hpp" #include "../mwmechanics/autocalcspell.hpp" #include "../mwmechanics/difficultyscaling.hpp" +#include "../mwmechanics/character.hpp" #include "../mwworld/ptr.hpp" #include "../mwworld/actiontalk.hpp" @@ -295,21 +296,6 @@ namespace MWClass MWWorld::LiveCellRef *ref = ptr.get(); - // NPC stats - if (!ref->mBase->mFaction.empty()) - { - std::string faction = ref->mBase->mFaction; - Misc::StringUtils::toLower(faction); - if(ref->mBase->mNpdtType != ESM::NPC::NPC_WITH_AUTOCALCULATED_STATS) - { - data->mNpcStats.getFactionRanks()[faction] = (int)ref->mBase->mNpdt52.mRank; - } - else - { - data->mNpcStats.getFactionRanks()[faction] = (int)ref->mBase->mNpdt12.mRank; - } - } - // creature stats int gold=0; if(ref->mBase->mNpdtType != ESM::NPC::NPC_WITH_AUTOCALCULATED_STATS) @@ -335,6 +321,8 @@ namespace MWClass data->mNpcStats.setLevel(ref->mBase->mNpdt52.mLevel); data->mNpcStats.setBaseDisposition(ref->mBase->mNpdt52.mDisposition); data->mNpcStats.setReputation(ref->mBase->mNpdt52.mReputation); + + data->mNpcStats.setNeedRecalcDynamicStats(false); } else { @@ -349,6 +337,8 @@ namespace MWClass autoCalculateAttributes(ref->mBase, data->mNpcStats); autoCalculateSkills(ref->mBase, data->mNpcStats, ptr); + + data->mNpcStats.setNeedRecalcDynamicStats(true); } // race powers @@ -356,16 +346,19 @@ namespace MWClass for (std::vector::const_iterator iter (race->mPowers.mList.begin()); iter!=race->mPowers.mList.end(); ++iter) { - data->mNpcStats.getSpells().add (*iter); + if (const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(*iter)) + data->mNpcStats.getSpells().add (spell); + else + std::cerr << "Warning: ignoring nonexistent race power '" << *iter << "' on NPC '" << ref->mBase->mId << "'" << std::endl; } - if (data->mNpcStats.getFactionRanks().size()) + if (!ref->mBase->mFaction.empty()) { static const int iAutoRepFacMod = MWBase::Environment::get().getWorld()->getStore().get() .find("iAutoRepFacMod")->getInt(); static const int iAutoRepLevMod = MWBase::Environment::get().getWorld()->getStore().get() .find("iAutoRepLevMod")->getInt(); - int rank = data->mNpcStats.getFactionRanks().begin()->second; + int rank = ref->mBase->getFactionRank(); data->mNpcStats.setReputation(iAutoRepFacMod * (rank+1) + iAutoRepLevMod * (data->mNpcStats.getLevel()-1)); } @@ -380,11 +373,19 @@ namespace MWClass // spells for (std::vector::const_iterator iter (ref->mBase->mSpells.mList.begin()); iter!=ref->mBase->mSpells.mList.end(); ++iter) - data->mNpcStats.getSpells().add (*iter); + { + if (const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(*iter)) + data->mNpcStats.getSpells().add (spell); + else + { + /// \todo add option to make this a fatal error message pop-up, but default to warning for vanilla compatibility + std::cerr << "Warning: ignoring nonexistent spell '" << *iter << "' on NPC '" << ref->mBase->mId << "'" << std::endl; + } + } // inventory - data->mInventoryStore.fill(ref->mBase->mInventory, getId(ptr), "", - MWBase::Environment::get().getWorld()->getStore()); + // setting ownership is used to make the NPC auto-equip his initial equipment only, and not bartered items + data->mInventoryStore.fill(ref->mBase->mInventory, getId(ptr)); data->mNpcStats.setGoldPool(gold); @@ -403,19 +404,19 @@ namespace MWClass return ref->mBase->mId; } - void Npc::adjustPosition(const MWWorld::Ptr& ptr) const + void Npc::adjustPosition(const MWWorld::Ptr& ptr, bool force) const { - MWBase::Environment::get().getWorld()->adjustPosition(ptr); + MWBase::Environment::get().getWorld()->adjustPosition(ptr, force); } - void Npc::insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const + void Npc::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { renderingInterface.getActors().insertNPC(ptr); } - void Npc::insertObject(const MWWorld::Ptr& ptr, MWWorld::PhysicsSystem& physics) const + void Npc::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWWorld::PhysicsSystem& physics) const { - physics.addActor(ptr); + physics.addActor(ptr, model); MWBase::Environment::get().getMechanicsManager()->add(ptr); if (getCreatureStats(ptr).isDead()) MWBase::Environment::get().getWorld()->enableActorCollision(ptr, false); @@ -485,18 +486,7 @@ namespace MWClass if(!weapon.isEmpty() && weapon.getTypeName() != typeid(ESM::Weapon).name()) weapon = MWWorld::Ptr(); - // Reduce fatigue - // somewhat of a guess, but using the weapon weight makes sense - const float fFatigueAttackBase = store.find("fFatigueAttackBase")->getFloat(); - const float fFatigueAttackMult = store.find("fFatigueAttackMult")->getFloat(); - const float fWeaponFatigueMult = store.find("fWeaponFatigueMult")->getFloat(); - MWMechanics::DynamicStat fatigue = getCreatureStats(ptr).getFatigue(); - const float normalizedEncumbrance = getEncumbrance(ptr) / getCapacity(ptr); - float fatigueLoss = fFatigueAttackBase + normalizedEncumbrance * fFatigueAttackMult; - if (!weapon.isEmpty()) - fatigueLoss += weapon.getClass().getWeight(weapon) * getNpcStats(ptr).getAttackStrength() * fWeaponFatigueMult; - fatigue.setCurrent(fatigue.getCurrent() - fatigueLoss); - getCreatureStats(ptr).setFatigue(fatigue); + MWMechanics::applyFatigueLoss(ptr, weapon); const float fCombatDistance = store.find("fCombatDistance")->getFloat(); float dist = fCombatDistance * (!weapon.isEmpty() ? @@ -529,24 +519,7 @@ namespace MWClass if((::rand()/(RAND_MAX+1.0)) > hitchance/100.0f) { othercls.onHit(victim, 0.0f, false, weapon, ptr, false); - - // Weapon health is reduced by 1 even if the attack misses - const bool weaphashealth = !weapon.isEmpty() && weapon.getClass().hasItemHealth(weapon); - if(weaphashealth) - { - int weaphealth = weapon.getClass().getItemHealth(weapon); - - if (!MWBase::Environment::get().getWorld()->getGodModeState()) - { - weaphealth -= std::min(1, weaphealth); - weapon.getCellRef().setCharge(weaphealth); - } - - // Weapon broken? unequip it - if (weaphealth == 0) - weapon = *inv.unequipItem(weapon, ptr); - } - + MWMechanics::reduceWeaponCondition(0.f, false, weapon, ptr); return; } @@ -555,7 +528,6 @@ namespace MWClass MWMechanics::NpcStats &stats = getNpcStats(ptr); if(!weapon.isEmpty()) { - const bool weaphashealth = weapon.getClass().hasItemHealth(weapon); const unsigned char *attack = NULL; if(type == ESM::Weapon::AT_Chop) attack = weapon.get()->mBase->mData.mChop; @@ -568,60 +540,14 @@ namespace MWClass damage = attack[0] + ((attack[1]-attack[0])*stats.getAttackStrength()); damage *= gmst.fDamageStrengthBase->getFloat() + (stats.getAttribute(ESM::Attribute::Strength).getModified() * gmst.fDamageStrengthMult->getFloat() * 0.1); - if(weaphashealth) - { - int weapmaxhealth = weapon.getClass().getItemMaxHealth(weapon); - int weaphealth = weapon.getClass().getItemHealth(weapon); - - damage *= float(weaphealth) / weapmaxhealth; - - if (!MWBase::Environment::get().getWorld()->getGodModeState()) - { - // Reduce weapon charge by at least one, but cap at 0 - weaphealth -= std::min(std::max(1, - (int)(damage * store.find("fWeaponDamageMult")->getFloat())), weaphealth); - - weapon.getCellRef().setCharge(weaphealth); - } - - // Weapon broken? unequip it - if (weaphealth == 0) - weapon = *inv.unequipItem(weapon, ptr); - } } + MWMechanics::adjustWeaponDamage(damage, weapon); + MWMechanics::reduceWeaponCondition(damage, true, weapon, ptr); healthdmg = true; } else { - // Note: MCP contains an option to include Strength in hand-to-hand damage - // calculations. Some mods recommend using it, so we may want to include am - // option for it. - float minstrike = store.find("fMinHandToHandMult")->getFloat(); - float maxstrike = store.find("fMaxHandToHandMult")->getFloat(); - damage = stats.getSkill(weapskill).getModified(); - damage *= minstrike + ((maxstrike-minstrike)*stats.getAttackStrength()); - - healthdmg = (otherstats.getMagicEffects().get(ESM::MagicEffect::Paralyze).mMagnitude > 0) - || otherstats.getKnockedDown(); - if(stats.isWerewolf()) - { - healthdmg = true; - // GLOB instead of GMST because it gets updated during a quest - const MWWorld::Store &glob = world->getStore().get(); - damage *= glob.find("WerewolfClawMult")->mValue.getFloat(); - } - if(healthdmg) - damage *= store.find("fHandtoHandHealthPer")->getFloat(); - - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - if(stats.isWerewolf()) - { - const ESM::Sound *sound = world->getStore().get().searchRandom("WolfHit"); - if(sound) - sndMgr->playSound3D(victim, sound->mId, 1.0f, 1.0f); - } - else - sndMgr->playSound3D(victim, "Hand To Hand Hit", 1.0f, 1.0f); + MWMechanics::getHandToHandDamage(ptr, victim, damage, healthdmg); } if(ptr.getRefData().getHandle() == "player") { @@ -658,7 +584,7 @@ namespace MWClass MWMechanics::applyElementalShields(ptr, victim); - if (!weapon.isEmpty() && MWMechanics::blockMeleeAttack(ptr, victim, weapon, damage)) + if (MWMechanics::blockMeleeAttack(ptr, victim, weapon, damage)) damage = 0; if (healthdmg && damage > 0) @@ -675,27 +601,22 @@ namespace MWClass // NOTE: 'object' and/or 'attacker' may be empty. - // Attacking peaceful NPCs is a crime - if (!attacker.isEmpty() && !ptr.getClass().getCreatureStats(ptr).isHostile() && - !MWBase::Environment::get().getMechanicsManager()->isAggressive(ptr, attacker)) - MWBase::Environment::get().getMechanicsManager()->commitCrime(attacker, ptr, MWBase::MechanicsManager::OT_Assault); + bool wasDead = getCreatureStats(ptr).isDead(); - if (!attacker.isEmpty() && attacker.getClass().getCreatureStats(attacker).getAiSequence().isInCombat(ptr) - && !ptr.getClass().getCreatureStats(ptr).getAiSequence().isInCombat(attacker)) + // Note OnPcHitMe is not set for friendly hits. + bool setOnPcHitMe = true; + if (!attacker.isEmpty() && !ptr.getClass().getCreatureStats(ptr).getAiSequence().isInCombat(attacker)) { - // Attacker is in combat with us, but we are not in combat with the attacker yet. Time to fight back. - // Note: accidental or collateral damage attacks are ignored. - MWBase::Environment::get().getMechanicsManager()->startCombat(ptr, attacker); - } + getCreatureStats(ptr).setAttacked(true); - bool wasDead = getCreatureStats(ptr).isDead(); + setOnPcHitMe = MWBase::Environment::get().getMechanicsManager()->actorAttacked(ptr, attacker); + } - getCreatureStats(ptr).setAttacked(true); + if(!object.isEmpty()) + getCreatureStats(ptr).setLastHitAttemptObject(object.getClass().getId(object)); if(!successful) { - // TODO: Handle HitAttemptOnMe script function - // Missed sndMgr->playSound3D(ptr, "miss", 1.0f, 1.0f); return; @@ -704,7 +625,7 @@ namespace MWClass if(!object.isEmpty()) getCreatureStats(ptr).setLastHitObject(object.getClass().getId(object)); - if(!attacker.isEmpty() && attacker.getRefData().getHandle() == "player") + if(setOnPcHitMe && !attacker.isEmpty() && attacker.getRefData().getHandle() == "player") { const std::string &script = ptr.getClass().getScript(ptr); /* Set the OnPCHitMe script variable. The script is responsible for clearing it. */ @@ -718,7 +639,7 @@ namespace MWClass if (damage < 0.001f) damage = 0; - if(damage > 0.0f) + if(damage > 0.0f && !attacker.isEmpty()) { // 'ptr' is losing health. Play a 'hit' voiced dialog entry if not already saying // something, alert the character controller, scripts, etc. @@ -746,7 +667,7 @@ namespace MWClass else getCreatureStats(ptr).setHitRecovery(true); // Is this supposed to always occur? - if(damage > 0 && ishealth && !attacker.isEmpty()) // Don't use armor mitigation for fall damage + if(damage > 0 && ishealth) { // Hit percentages: // cuirass = 30% @@ -814,7 +735,11 @@ namespace MWClass damage = scaleDamage(damage, attacker, ptr); if(damage > 0.0f) + { sndMgr->playSound3D(ptr, "Health Damage", 1.0f, 1.0f); + if (ptr.getRefData().getHandle() == "player") + MWBase::Environment::get().getWindowManager()->activateHitOverlay(); + } float health = getCreatureStats(ptr).getHealth().getCurrent() - damage; setActorHealth(ptr, health, attacker); } @@ -907,11 +832,11 @@ namespace MWClass if(getCreatureStats(ptr).isDead()) return boost::shared_ptr(new MWWorld::ActionOpen(ptr, true)); - if(ptr.getClass().getCreatureStats(ptr).isHostile()) + if(ptr.getClass().getCreatureStats(ptr).getAiSequence().isInCombat()) return boost::shared_ptr(new MWWorld::FailedAction("#{sActorInCombat}")); - if(getCreatureStats(actor).getStance(MWMechanics::CreatureStats::Stance_Sneak)) + if(getCreatureStats(actor).getStance(MWMechanics::CreatureStats::Stance_Sneak) + || ptr.getClass().getCreatureStats(ptr).getKnockedDown()) return boost::shared_ptr(new MWWorld::ActionOpen(ptr)); // stealing - // Can't talk to werewolfs if(ptr.getClass().isNpc() && ptr.getClass().getNpcStats(ptr).isWerewolf()) return boost::shared_ptr (new MWWorld::FailedAction("")); @@ -950,7 +875,7 @@ namespace MWClass const NpcCustomData *npcdata = static_cast(ptr.getRefData().getCustomData()); const MWMechanics::MagicEffects &mageffects = npcdata->mNpcStats.getMagicEffects(); - const float normalizedEncumbrance = Npc::getEncumbrance(ptr) / Npc::getCapacity(ptr); + const float normalizedEncumbrance = getNormalizedEncumbrance(ptr); bool sneaking = ptr.getClass().getCreatureStats(ptr).getStance(MWMechanics::CreatureStats::Stance_Sneak); bool running = ptr.getClass().getCreatureStats(ptr).getStance(MWMechanics::CreatureStats::Stance_Run); @@ -964,17 +889,15 @@ namespace MWClass float runSpeed = walkSpeed*(0.01f * npcdata->mNpcStats.getSkill(ESM::Skill::Athletics).getModified() * gmst.fAthleticsRunBonus->getFloat() + gmst.fBaseRunMultiplier->getFloat()); - if(npcdata->mNpcStats.isWerewolf()) - runSpeed *= gmst.fWereWolfRunMult->getFloat(); float moveSpeed; - if(normalizedEncumbrance >= 1.0f) + if(getEncumbrance(ptr) > getCapacity(ptr)) moveSpeed = 0.0f; - else if(mageffects.get(ESM::MagicEffect::Levitate).mMagnitude > 0 && + else if(mageffects.get(ESM::MagicEffect::Levitate).getMagnitude() > 0 && world->isLevitationEnabled()) { float flySpeed = 0.01f*(npcdata->mNpcStats.getAttribute(ESM::Attribute::Speed).getModified() + - mageffects.get(ESM::MagicEffect::Levitate).mMagnitude); + mageffects.get(ESM::MagicEffect::Levitate).getMagnitude()); flySpeed = gmst.fMinFlySpeed->getFloat() + flySpeed*(gmst.fMaxFlySpeed->getFloat() - gmst.fMinFlySpeed->getFloat()); flySpeed *= 1.0f - gmst.fEncumberedMoveEffect->getFloat() * normalizedEncumbrance; flySpeed = std::max(0.0f, flySpeed); @@ -985,7 +908,7 @@ namespace MWClass float swimSpeed = walkSpeed; if(running) swimSpeed = runSpeed; - swimSpeed *= 1.0f + 0.01f * mageffects.get(ESM::MagicEffect::SwiftSwim).mMagnitude; + swimSpeed *= 1.0f + 0.01f * mageffects.get(ESM::MagicEffect::SwiftSwim).getMagnitude(); swimSpeed *= gmst.fSwimRunBase->getFloat() + 0.01f*npcdata->mNpcStats.getSkill(ESM::Skill::Athletics).getModified()* gmst.fSwimRunAthleticsMult->getFloat(); moveSpeed = swimSpeed; @@ -997,11 +920,17 @@ namespace MWClass if(getMovementSettings(ptr).mPosition[0] != 0 && getMovementSettings(ptr).mPosition[1] == 0) moveSpeed *= 0.75f; + if(npcdata->mNpcStats.isWerewolf() && running && npcdata->mNpcStats.getDrawState() == MWMechanics::DrawState_Nothing) + moveSpeed *= gmst.fWereWolfRunMult->getFloat(); + return moveSpeed; } float Npc::getJump(const MWWorld::Ptr &ptr) const { + if(getEncumbrance(ptr) > getCapacity(ptr)) + return 0.f; + const NpcCustomData *npcdata = static_cast(ptr.getRefData().getCustomData()); const GMST& gmst = getGmst(); const MWMechanics::MagicEffects &mageffects = npcdata->mNpcStats.getMagicEffects(); @@ -1020,7 +949,7 @@ namespace MWClass float x = gmst.fJumpAcrobaticsBase->getFloat() + std::pow(a / 15.0f, gmst.fJumpAcroMultiplier->getFloat()); x += 3.0f * b * gmst.fJumpAcroMultiplier->getFloat(); - x += mageffects.get(ESM::MagicEffect::Jump).mMagnitude * 64; + x += mageffects.get(ESM::MagicEffect::Jump).getMagnitude() * 64; x *= encumbranceTerm; if(ptr.getClass().getCreatureStats(ptr).getStance(MWMechanics::CreatureStats::Stance_Run)) @@ -1032,37 +961,6 @@ namespace MWClass return x; } - float Npc::getFallDamage(const MWWorld::Ptr &ptr, float fallHeight) const - { - MWBase::World *world = MWBase::Environment::get().getWorld(); - const MWWorld::Store &store = world->getStore().get(); - - const float fallDistanceMin = store.find("fFallDamageDistanceMin")->getFloat(); - - if (fallHeight >= fallDistanceMin) - { - const float acrobaticsSkill = ptr.getClass().getNpcStats (ptr).getSkill(ESM::Skill::Acrobatics).getModified(); - const NpcCustomData *npcdata = static_cast(ptr.getRefData().getCustomData()); - const float jumpSpellBonus = npcdata->mNpcStats.getMagicEffects().get(ESM::MagicEffect::Jump).mMagnitude; - const float fallAcroBase = store.find("fFallAcroBase")->getFloat(); - const float fallAcroMult = store.find("fFallAcroMult")->getFloat(); - const float fallDistanceBase = store.find("fFallDistanceBase")->getFloat(); - const float fallDistanceMult = store.find("fFallDistanceMult")->getFloat(); - - float x = fallHeight - fallDistanceMin; - x -= (1.5 * acrobaticsSkill) + jumpSpellBonus; - x = std::max(0.0f, x); - - float a = fallAcroBase + fallAcroMult * (100 - acrobaticsSkill); - x = fallDistanceBase + fallDistanceMult * x; - x *= a; - - return x; - } - - return 0; - } - MWMechanics::Movement& Npc::getMovementSettings (const MWWorld::Ptr& ptr) const { ensureCustomData (ptr); @@ -1106,9 +1004,7 @@ namespace MWClass bool Npc::hasToolTip (const MWWorld::Ptr& ptr) const { - /// \todo We don't want tooltips for NPCs in combat mode. - - return true; + return !ptr.getClass().getCreatureStats(ptr).getAiSequence().isInCombat() || getCreatureStats(ptr).isDead(); } MWGui::ToolTipInfo Npc::getToolTipInfo (const MWWorld::Ptr& ptr) const @@ -1149,8 +1045,8 @@ namespace MWClass if(!stats.isWerewolf()) { weight = getContainerStore(ptr).getWeight(); - weight -= stats.getMagicEffects().get(ESM::MagicEffect::Feather).mMagnitude; - weight += stats.getMagicEffects().get(ESM::MagicEffect::Burden).mMagnitude; + weight -= stats.getMagicEffects().get(ESM::MagicEffect::Feather).getMagnitude(); + weight += stats.getMagicEffects().get(ESM::MagicEffect::Burden).getMagnitude(); if(weight < 0.0f) weight = 0.0f; } @@ -1165,10 +1061,13 @@ namespace MWClass return cast.cast(id); } - void Npc::skillUsageSucceeded (const MWWorld::Ptr& ptr, int skill, int usageType) const + void Npc::skillUsageSucceeded (const MWWorld::Ptr& ptr, int skill, int usageType, float extraFactor) const { MWMechanics::NpcStats& stats = getNpcStats (ptr); + if (stats.isWerewolf()) + return; + MWWorld::LiveCellRef *ref = ptr.get(); const ESM::Class *class_ = @@ -1176,7 +1075,7 @@ namespace MWClass ref->mBase->mClass ); - stats.useSkill (skill, *class_, usageType); + stats.useSkill (skill, *class_, usageType, extraFactor); } float Npc::getArmorRating (const MWWorld::Ptr& ptr) const @@ -1187,7 +1086,6 @@ namespace MWClass MWMechanics::NpcStats &stats = getNpcStats(ptr); MWWorld::InventoryStore &invStore = getInventoryStore(ptr); - int iBaseArmorSkill = store.find("iBaseArmorSkill")->getInt(); float fUnarmoredBase1 = store.find("fUnarmoredBase1")->getFloat(); float fUnarmoredBase2 = store.find("fUnarmoredBase2")->getFloat(); int unarmoredSkill = stats.getSkill(ESM::Skill::Unarmored).getModified(); @@ -1203,19 +1101,11 @@ namespace MWClass } else { - MWWorld::LiveCellRef *ref = it->get(); - - int armorSkillType = it->getClass().getEquipmentSkill(*it); - int armorSkill = stats.getSkill(armorSkillType).getModified(); - - if(ref->mBase->mData.mWeight == 0) - ratings[i] = ref->mBase->mData.mArmor; - else - ratings[i] = ref->mBase->mData.mArmor * armorSkill / iBaseArmorSkill; + ratings[i] = it->getClass().getEffectiveArmorRating(*it, ptr); } } - float shield = stats.getMagicEffects().get(ESM::MagicEffect::Shield).mMagnitude; + float shield = stats.getMagicEffects().get(ESM::MagicEffect::Shield).getMagnitude(); return ratings[MWWorld::InventoryStore::Slot_Cuirass] * 0.3f + (ratings[MWWorld::InventoryStore::Slot_CarriedLeft] + ratings[MWWorld::InventoryStore::Slot_Helmet] @@ -1227,13 +1117,6 @@ namespace MWClass + shield; } - - void Npc::adjustRotation(const MWWorld::Ptr& ptr,float& x,float& y,float& z) const - { - y = 0; - x = 0; - } - void Npc::adjustScale(const MWWorld::Ptr &ptr, float &scale) const { MWWorld::LiveCellRef *ref = @@ -1260,65 +1143,48 @@ namespace MWClass std::string Npc::getSoundIdFromSndGen(const MWWorld::Ptr &ptr, const std::string &name) const { - if(name == "left") + if(name == "left" || name == "right") { MWBase::World *world = MWBase::Environment::get().getWorld(); Ogre::Vector3 pos(ptr.getRefData().getPosition().pos); if(world->isSwimming(ptr)) - return "Swim Left"; - if(world->isUnderwater(ptr.getCell(), pos)) - return "FootWaterLeft"; + return (name == "left") ? "Swim Left" : "Swim Right"; + if(world->isUnderwater(ptr.getCell(), pos) || world->isWalkingOnWater(ptr)) + return (name == "left") ? "FootWaterLeft" : "FootWaterRight"; if(world->isOnGround(ptr)) { - MWWorld::InventoryStore &inv = Npc::getInventoryStore(ptr); - MWWorld::ContainerStoreIterator boots = inv.getSlot(MWWorld::InventoryStore::Slot_Boots); - if(boots == inv.end() || boots->getTypeName() != typeid(ESM::Armor).name()) - return "FootBareLeft"; - - switch(boots->getClass().getEquipmentSkill(*boots)) + if (ptr.getClass().getNpcStats(ptr).isWerewolf() + && ptr.getClass().getCreatureStats(ptr).getStance(MWMechanics::CreatureStats::Stance_Run)) { - case ESM::Skill::LightArmor: - return "FootLightLeft"; - case ESM::Skill::MediumArmor: - return "FootMedLeft"; - case ESM::Skill::HeavyArmor: - return "FootHeavyLeft"; + MWMechanics::WeaponType weaponType = MWMechanics::WeapType_None; + MWMechanics::getActiveWeapon(ptr.getClass().getCreatureStats(ptr), ptr.getClass().getInventoryStore(ptr), &weaponType); + if (weaponType == MWMechanics::WeapType_None) + return ""; } - } - return ""; - } - if(name == "right") - { - MWBase::World *world = MWBase::Environment::get().getWorld(); - Ogre::Vector3 pos(ptr.getRefData().getPosition().pos); - if(world->isSwimming(ptr)) - return "Swim Right"; - if(world->isUnderwater(ptr.getCell(), pos)) - return "FootWaterRight"; - if(world->isOnGround(ptr)) - { + MWWorld::InventoryStore &inv = Npc::getInventoryStore(ptr); MWWorld::ContainerStoreIterator boots = inv.getSlot(MWWorld::InventoryStore::Slot_Boots); if(boots == inv.end() || boots->getTypeName() != typeid(ESM::Armor).name()) - return "FootBareRight"; + return (name == "left") ? "FootBareLeft" : "FootBareRight"; switch(boots->getClass().getEquipmentSkill(*boots)) { case ESM::Skill::LightArmor: - return "FootLightRight"; + return (name == "left") ? "FootLightLeft" : "FootLightRight"; case ESM::Skill::MediumArmor: - return "FootMedRight"; + return (name == "left") ? "FootMedLeft" : "FootMedRight"; case ESM::Skill::HeavyArmor: - return "FootHeavyRight"; + return (name == "left") ? "FootHeavyLeft" : "FootHeavyRight"; } } return ""; } + if(name == "land") { MWBase::World *world = MWBase::Environment::get().getWorld(); Ogre::Vector3 pos(ptr.getRefData().getPosition().pos); - if(world->isUnderwater(ptr.getCell(), pos)) + if(world->isUnderwater(ptr.getCell(), pos) || world->isWalkingOnWater(ptr)) return "DefaultLandWater"; if(world->isOnGround(ptr)) return "Body Fall Medium"; @@ -1331,6 +1197,7 @@ namespace MWClass // TODO: I have no idea what these are supposed to do for NPCs since they use // voiced dialog for various conditions like health loss and combat taunts. Maybe // only for biped creatures? + if(name == "moan") return ""; if(name == "roar") @@ -1369,6 +1236,9 @@ namespace MWClass void Npc::readAdditionalState (const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const { + if (!state.mHasCustomState) + return; + const ESM::NpcState& state2 = dynamic_cast (state); ensureCustomData(ptr); @@ -1396,6 +1266,12 @@ namespace MWClass { ESM::NpcState& state2 = dynamic_cast (state); + if (!ptr.getRefData().getCustomData()) + { + state.mHasCustomState = false; + return; + } + ensureCustomData (ptr); NpcCustomData& customData = dynamic_cast (*ptr.getRefData().getCustomData()); @@ -1425,7 +1301,7 @@ namespace MWClass { // Note we do not respawn moved references in the cell they were moved to. Instead they are respawned in the original cell. // This also means we cannot respawn dynamically placed references with no content file connection. - if (ptr.getCellRef().getRefNum().mContentFile != -1) + if (ptr.getCellRef().hasContentFile()) { if (ptr.getRefData().getCount() == 0) ptr.getRefData().setCount(1); @@ -1443,6 +1319,29 @@ namespace MWClass MWWorld::LiveCellRef *ref = ptr.get(); const ESM::InventoryList& list = ref->mBase->mInventory; MWWorld::ContainerStore& store = getContainerStore(ptr); - store.restock(list, ptr, ptr.getCellRef().getRefId(), ptr.getCellRef().getFaction()); + store.restock(list, ptr, ptr.getCellRef().getRefId()); + } + + int Npc::getBaseFightRating (const MWWorld::Ptr& ptr) const + { + MWWorld::LiveCellRef *ref = ptr.get(); + return ref->mBase->mAiData.mFight; + } + + bool Npc::isBipedal(const MWWorld::Ptr &ptr) const + { + return true; + } + + std::string Npc::getPrimaryFaction (const MWWorld::Ptr& ptr) const + { + MWWorld::LiveCellRef *ref = ptr.get(); + return ref->mBase->mFaction; + } + + int Npc::getPrimaryFactionRank (const MWWorld::Ptr& ptr) const + { + MWWorld::LiveCellRef *ref = ptr.get(); + return ref->mBase->getFactionRank(); } } diff --git a/apps/openmw/mwclass/npc.hpp b/apps/openmw/mwclass/npc.hpp index b0ca0a658..9aece7368 100644 --- a/apps/openmw/mwclass/npc.hpp +++ b/apps/openmw/mwclass/npc.hpp @@ -50,12 +50,14 @@ namespace MWClass virtual std::string getId (const MWWorld::Ptr& ptr) const; ///< Return ID of \a ptr - virtual void insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const; + virtual void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const; ///< Add reference into a cell for rendering - virtual void insertObject(const MWWorld::Ptr& ptr, MWWorld::PhysicsSystem& physics) const; + virtual void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWWorld::PhysicsSystem& physics) const; - virtual void adjustPosition(const MWWorld::Ptr& ptr) const; + virtual void adjustPosition(const MWWorld::Ptr& ptr, bool force) const; + ///< Adjust position to stand on ground. Must be called post model load + /// @param force do this even if the ptr is flying virtual std::string getName (const MWWorld::Ptr& ptr) const; ///< \return name (the one that is to be presented to the user; not the internal one); @@ -102,9 +104,6 @@ namespace MWClass virtual float getJump(const MWWorld::Ptr &ptr) const; ///< Return jump velocity (not accounting for movement) - virtual float getFallDamage(const MWWorld::Ptr &ptr, float fallHeight) const; - ///< Return amount of health points lost when falling - virtual MWMechanics::Movement& getMovementSettings (const MWWorld::Ptr& ptr) const; ///< Return desired movement. @@ -134,11 +133,9 @@ namespace MWClass virtual void adjustScale (const MWWorld::Ptr &ptr, float &scale) const; - virtual void skillUsageSucceeded (const MWWorld::Ptr& ptr, int skill, int usageType) const; + virtual void skillUsageSucceeded (const MWWorld::Ptr& ptr, int skill, int usageType, float extraFactor=1.f) const; ///< Inform actor \a ptr that a skill use has succeeded. - virtual void adjustRotation(const MWWorld::Ptr& ptr,float& x,float& y,float& z) const; - virtual bool isEssential (const MWWorld::Ptr& ptr) const; ///< Is \a ptr essential? (i.e. may losing \a ptr make the game unwinnable) @@ -185,9 +182,16 @@ namespace MWClass return true; } + virtual bool isBipedal (const MWWorld::Ptr &ptr) const; + virtual void respawn (const MWWorld::Ptr& ptr) const; virtual void restock (const MWWorld::Ptr& ptr) const; + + virtual int getBaseFightRating (const MWWorld::Ptr& ptr) const; + + virtual std::string getPrimaryFaction(const MWWorld::Ptr &ptr) const; + virtual int getPrimaryFactionRank(const MWWorld::Ptr &ptr) const; }; } diff --git a/apps/openmw/mwclass/potion.cpp b/apps/openmw/mwclass/potion.cpp index 734385a6a..bd06f89fc 100644 --- a/apps/openmw/mwclass/potion.cpp +++ b/apps/openmw/mwclass/potion.cpp @@ -11,6 +11,7 @@ #include "../mwworld/actiontake.hpp" #include "../mwworld/actionapply.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/physicssystem.hpp" #include "../mwworld/nullaction.hpp" @@ -24,19 +25,22 @@ namespace MWClass { - void Potion::insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const + std::string Potion::getId (const MWWorld::Ptr& ptr) const + { + return ptr.get()->mBase->mId; + } + + void Potion::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { - const std::string model = getModel(ptr); if (!model.empty()) { renderingInterface.getObjects().insertModel(ptr, model); } } - void Potion::insertObject(const MWWorld::Ptr& ptr, MWWorld::PhysicsSystem& physics) const + void Potion::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWWorld::PhysicsSystem& physics) const { - const std::string model = getModel(ptr); if(!model.empty()) - physics.addObject(ptr,true); + physics.addObject(ptr, model, true); } std::string Potion::getModel(const MWWorld::Ptr &ptr) const @@ -140,11 +144,8 @@ namespace MWClass MWBase::Environment::get().getWorld()->getStore().get().find("fWortChanceValue")->getFloat(); for (MWGui::Widgets::SpellEffectList::iterator it = info.effects.begin(); it != info.effects.end(); ++it) { - it->mKnown = ( (i == 0 && alchemySkill >= fWortChanceValue) - || (i == 1 && alchemySkill >= fWortChanceValue*2) - || (i == 2 && alchemySkill >= fWortChanceValue*3) - || (i == 3 && alchemySkill >= fWortChanceValue*4)); - + it->mKnown = (i <= 1 && alchemySkill >= fWortChanceValue) + || (i <= 3 && alchemySkill >= fWortChanceValue*2); ++i; } diff --git a/apps/openmw/mwclass/potion.hpp b/apps/openmw/mwclass/potion.hpp index 0f0578ca0..32e390115 100644 --- a/apps/openmw/mwclass/potion.hpp +++ b/apps/openmw/mwclass/potion.hpp @@ -12,10 +12,13 @@ namespace MWClass public: - virtual void insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const; + /// Return ID of \a ptr + virtual std::string getId (const MWWorld::Ptr& ptr) const; + + virtual void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const; ///< Add reference into a cell for rendering - virtual void insertObject(const MWWorld::Ptr& ptr, MWWorld::PhysicsSystem& physics) const; + virtual void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWWorld::PhysicsSystem& physics) const; virtual std::string getName (const MWWorld::Ptr& ptr) const; ///< \return name (the one that is to be presented to the user; not the internal one); diff --git a/apps/openmw/mwclass/probe.cpp b/apps/openmw/mwclass/probe.cpp index 5df7da2b0..a11725f26 100644 --- a/apps/openmw/mwclass/probe.cpp +++ b/apps/openmw/mwclass/probe.cpp @@ -22,19 +22,22 @@ namespace MWClass { - void Probe::insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const + std::string Probe::getId (const MWWorld::Ptr& ptr) const + { + return ptr.get()->mBase->mId; + } + + void Probe::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { - const std::string model = getModel(ptr); if (!model.empty()) { renderingInterface.getObjects().insertModel(ptr, model); } } - void Probe::insertObject(const MWWorld::Ptr& ptr, MWWorld::PhysicsSystem& physics) const + void Probe::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWWorld::PhysicsSystem& physics) const { - const std::string model = getModel(ptr); if(!model.empty()) - physics.addObject(ptr,true); + physics.addObject(ptr, model, true); } std::string Probe::getModel(const MWWorld::Ptr &ptr) const diff --git a/apps/openmw/mwclass/probe.hpp b/apps/openmw/mwclass/probe.hpp index a959e6c42..bb90ac153 100644 --- a/apps/openmw/mwclass/probe.hpp +++ b/apps/openmw/mwclass/probe.hpp @@ -12,10 +12,13 @@ namespace MWClass public: - virtual void insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const; + /// Return ID of \a ptr + virtual std::string getId (const MWWorld::Ptr& ptr) const; + + virtual void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const; ///< Add reference into a cell for rendering - virtual void insertObject(const MWWorld::Ptr& ptr, MWWorld::PhysicsSystem& physics) const; + virtual void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWWorld::PhysicsSystem& physics) const; virtual std::string getName (const MWWorld::Ptr& ptr) const; ///< \return name (the one that is to be presented to the user; not the internal one); diff --git a/apps/openmw/mwclass/repair.cpp b/apps/openmw/mwclass/repair.cpp index ed09a06da..e9c4ac9b1 100644 --- a/apps/openmw/mwclass/repair.cpp +++ b/apps/openmw/mwclass/repair.cpp @@ -21,19 +21,22 @@ namespace MWClass { - void Repair::insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const + std::string Repair::getId (const MWWorld::Ptr& ptr) const + { + return ptr.get()->mBase->mId; + } + + void Repair::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { - const std::string model = getModel(ptr); if (!model.empty()) { renderingInterface.getObjects().insertModel(ptr, model); } } - void Repair::insertObject(const MWWorld::Ptr& ptr, MWWorld::PhysicsSystem& physics) const + void Repair::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWWorld::PhysicsSystem& physics) const { - const std::string model = getModel(ptr); if(!model.empty()) - physics.addObject(ptr,true); + physics.addObject(ptr, model, true); } std::string Repair::getModel(const MWWorld::Ptr &ptr) const diff --git a/apps/openmw/mwclass/repair.hpp b/apps/openmw/mwclass/repair.hpp index 28ca5ad4c..2589a4af3 100644 --- a/apps/openmw/mwclass/repair.hpp +++ b/apps/openmw/mwclass/repair.hpp @@ -12,10 +12,13 @@ namespace MWClass public: - virtual void insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const; + /// Return ID of \a ptr + virtual std::string getId (const MWWorld::Ptr& ptr) const; + + virtual void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const; ///< Add reference into a cell for rendering - virtual void insertObject(const MWWorld::Ptr& ptr, MWWorld::PhysicsSystem& physics) const; + virtual void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWWorld::PhysicsSystem& physics) const; virtual std::string getName (const MWWorld::Ptr& ptr) const; ///< \return name (the one that is to be presented to the user; not the internal one); @@ -60,7 +63,7 @@ namespace MWClass virtual int getItemMaxHealth (const MWWorld::Ptr& ptr) const; ///< Return item max health or throw an exception, if class does not have item health - /// (default implementation: throw an exceoption) + /// (default implementation: throw an exception) virtual float getWeight (const MWWorld::Ptr& ptr) const; diff --git a/apps/openmw/mwclass/static.cpp b/apps/openmw/mwclass/static.cpp index 8768bde06..dbbe7e43a 100644 --- a/apps/openmw/mwclass/static.cpp +++ b/apps/openmw/mwclass/static.cpp @@ -12,22 +12,25 @@ namespace MWClass { - void Static::insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const + std::string Static::getId (const MWWorld::Ptr& ptr) const + { + return ptr.get()->mBase->mId; + } + + void Static::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { MWWorld::LiveCellRef *ref = ptr.get(); - const std::string model = getModel(ptr); if (!model.empty()) { renderingInterface.getObjects().insertModel(ptr, model, !ref->mBase->mPersistent); } } - void Static::insertObject(const MWWorld::Ptr& ptr, MWWorld::PhysicsSystem& physics) const + void Static::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWWorld::PhysicsSystem& physics) const { - const std::string model = getModel(ptr); if(!model.empty()) - physics.addObject(ptr); + physics.addObject(ptr, model); } std::string Static::getModel(const MWWorld::Ptr &ptr) const diff --git a/apps/openmw/mwclass/static.hpp b/apps/openmw/mwclass/static.hpp index e36b3d142..a94dff394 100644 --- a/apps/openmw/mwclass/static.hpp +++ b/apps/openmw/mwclass/static.hpp @@ -12,10 +12,13 @@ namespace MWClass public: - virtual void insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const; + /// Return ID of \a ptr + virtual std::string getId (const MWWorld::Ptr& ptr) const; + + virtual void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const; ///< Add reference into a cell for rendering - virtual void insertObject(const MWWorld::Ptr& ptr, MWWorld::PhysicsSystem& physics) const; + virtual void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWWorld::PhysicsSystem& physics) const; virtual std::string getName (const MWWorld::Ptr& ptr) const; ///< \return name (the one that is to be presented to the user; not the internal one); diff --git a/apps/openmw/mwclass/weapon.cpp b/apps/openmw/mwclass/weapon.cpp index 6f75095ff..d1a44fd0e 100644 --- a/apps/openmw/mwclass/weapon.cpp +++ b/apps/openmw/mwclass/weapon.cpp @@ -12,6 +12,7 @@ #include "../mwworld/actionequip.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwworld/physicssystem.hpp" #include "../mwworld/nullaction.hpp" @@ -29,19 +30,17 @@ namespace MWClass return ref->mBase->mId; } - void Weapon::insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const + void Weapon::insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { - const std::string model = getModel(ptr); if (!model.empty()) { renderingInterface.getObjects().insertModel(ptr, model); } } - void Weapon::insertObject(const MWWorld::Ptr& ptr, MWWorld::PhysicsSystem& physics) const + void Weapon::insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWWorld::PhysicsSystem& physics) const { - const std::string model = getModel(ptr); if(!model.empty()) - physics.addObject(ptr,true); + physics.addObject(ptr, model, true); } std::string Weapon::getModel(const MWWorld::Ptr &ptr) const @@ -190,9 +189,8 @@ namespace MWClass { return std::string("Item Weapon Longblade Up"); } - // Shortblade and thrown weapons - // thrown weapons may not be entirely correct - if (type == 0 || type == 11) + // Shortblade + if (type == 0) { return std::string("Item Weapon Shortblade Up"); } @@ -201,8 +199,8 @@ namespace MWClass { return std::string("Item Weapon Spear Up"); } - // Blunts and Axes - if (type == 3 || type == 4 || type == 5 || type == 7 || type == 8) + // Blunts, Axes and Thrown weapons + if (type == 3 || type == 4 || type == 5 || type == 7 || type == 8 || type == 11) { return std::string("Item Weapon Blunt Up"); } @@ -236,9 +234,8 @@ namespace MWClass { return std::string("Item Weapon Longblade Down"); } - // Shortblade and thrown weapons - // thrown weapons may not be entirely correct - if (type == 0 || type == 11) + // Shortblade + if (type == 0) { return std::string("Item Weapon Shortblade Down"); } @@ -247,8 +244,8 @@ namespace MWClass { return std::string("Item Weapon Spear Down"); } - // Blunts and Axes - if (type == 3 || type == 4 || type == 5 || type == 7 || type == 8) + // Blunts, Axes and Thrown weapons + if (type == 3 || type == 4 || type == 5 || type == 7 || type == 8 || type == 11) { return std::string("Item Weapon Blunt Down"); } @@ -378,13 +375,14 @@ namespace MWClass newItem.mName=newName; newItem.mData.mEnchant=enchCharge; newItem.mEnchant=enchId; + newItem.mData.mFlags |= ESM::Weapon::Magical; const ESM::Weapon *record = MWBase::Environment::get().getWorld()->createRecord (newItem); return record->mId; } std::pair Weapon::canBeEquipped(const MWWorld::Ptr &ptr, const MWWorld::Ptr &npc) const { - if (ptr.getCellRef().getCharge() == 0) + if (hasItemHealth(ptr) && ptr.getCellRef().getCharge() == 0) return std::make_pair(0, "#{sInventoryMessage1}"); std::pair, bool> slots_ = ptr.getClass().getEquipmentSlots(ptr); @@ -434,7 +432,8 @@ namespace MWClass bool Weapon::canSell (const MWWorld::Ptr& item, int npcServices) const { - return npcServices & ESM::NPC::Weapon; + return (npcServices & ESM::NPC::Weapon) + || ((npcServices & ESM::NPC::MagicItems) && !getEnchantment(item).empty()); } float Weapon::getWeight(const MWWorld::Ptr &ptr) const diff --git a/apps/openmw/mwclass/weapon.hpp b/apps/openmw/mwclass/weapon.hpp index 97ee10291..47f1c5251 100644 --- a/apps/openmw/mwclass/weapon.hpp +++ b/apps/openmw/mwclass/weapon.hpp @@ -15,10 +15,10 @@ namespace MWClass virtual std::string getId (const MWWorld::Ptr& ptr) const; ///< Return ID of \a ptr - virtual void insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const; + virtual void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const; ///< Add reference into a cell for rendering - virtual void insertObject(const MWWorld::Ptr& ptr, MWWorld::PhysicsSystem& physics) const; + virtual void insertObject(const MWWorld::Ptr& ptr, const std::string& model, MWWorld::PhysicsSystem& physics) const; virtual std::string getName (const MWWorld::Ptr& ptr) const; ///< \return name (the one that is to be presented to the user; not the internal one); diff --git a/apps/openmw/mwdialogue/dialoguemanagerimp.cpp b/apps/openmw/mwdialogue/dialoguemanagerimp.cpp index 6c801f755..1d1f655aa 100644 --- a/apps/openmw/mwdialogue/dialoguemanagerimp.cpp +++ b/apps/openmw/mwdialogue/dialoguemanagerimp.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -42,11 +43,12 @@ #include "../mwmechanics/npcstats.hpp" #include "filter.hpp" +#include "hypertextparser.hpp" namespace MWDialogue { DialogueManager::DialogueManager (const Compiler::Extensions& extensions, bool scriptVerbose, Translation::Storage& translationDataStorage) : - mCompilerContext (MWScript::CompilerContext::Type_Dialgoue), + mCompilerContext (MWScript::CompilerContext::Type_Dialogue), mErrorStream(std::cout.rdbuf()),mErrorHandler(mErrorStream) , mTemporaryDispositionChange(0.f) , mPermanentDispositionChange(0.f), mScriptVerbose (scriptVerbose) @@ -77,47 +79,32 @@ namespace MWDialogue void DialogueManager::addTopic (const std::string& topic) { - mKnownTopics[Misc::StringUtils::lowerCase(topic)] = true; + mKnownTopics.insert( Misc::StringUtils::lowerCase(topic) ); } void DialogueManager::parseText (const std::string& text) { - std::vector hypertext = ParseHyperText(text); + std::vector hypertext = HyperTextParser::parseHyperText(text); - //calculation of standard form fir all hyperlinks - for (size_t i = 0; i < hypertext.size(); ++i) + for (std::vector::iterator tok = hypertext.begin(); tok != hypertext.end(); ++tok) { - if (hypertext[i].mLink) + std::string topicId = Misc::StringUtils::lowerCase(tok->mText); + + if (tok->isExplicitLink()) { - size_t asterisk_count = MWDialogue::RemovePseudoAsterisks(hypertext[i].mText); + // calculation of standard form for all hyperlinks + size_t asterisk_count = HyperTextParser::removePseudoAsterisks(topicId); for(; asterisk_count > 0; --asterisk_count) - hypertext[i].mText.append("*"); + topicId.append("*"); - hypertext[i].mText = mTranslationDataStorage.topicStandardForm(hypertext[i].mText); + topicId = mTranslationDataStorage.topicStandardForm(topicId); } - } - for (size_t i = 0; i < hypertext.size(); ++i) - { - std::list::iterator it; - for(it = mActorKnownTopics.begin(); it != mActorKnownTopics.end(); ++it) - { - if (hypertext[i].mLink) - { - if( hypertext[i].mText == *it ) - { - mKnownTopics[hypertext[i].mText] = true; - } - } - else if( !mTranslationDataStorage.hasTranslation() ) - { - size_t pos = Misc::StringUtils::lowerCase(hypertext[i].mText).find(*it, 0); - if(pos !=std::string::npos) - { - mKnownTopics[*it] = true; - } - } - } + if (tok->isImplicitKeyword() && mTranslationDataStorage.hasTranslation()) + continue; + + if (mActorKnownTopics.count( topicId )) + mKnownTopics.insert( topicId ); } updateTopics(); @@ -241,7 +228,7 @@ namespace MWDialogue success = false; } - if (!success && mScriptVerbose) + if (!success) { std::cerr << "compiling failed (dialogue script)" << std::endl @@ -290,6 +277,9 @@ namespace MWDialogue std::string title; if (dialogue.mType==ESM::Dialogue::Persuasion) { + // Determine GMST from dialogue topic. GMSTs are: + // sAdmireSuccess, sAdmireFail, sIntimidateSuccess, sIntimidateFail, + // sTauntSuccess, sTauntFail, sBribeSuccess, sBribeFail std::string modifiedTopic = "s" + topic; modifiedTopic.erase (std::remove (modifiedTopic.begin(), modifiedTopic.end(), ' '), modifiedTopic.end()); @@ -352,10 +342,10 @@ namespace MWDialogue if (filter.responseAvailable (*iter)) { std::string lower = Misc::StringUtils::lowerCase(iter->mId); - mActorKnownTopics.push_back (lower); + mActorKnownTopics.insert (lower); //does the player know the topic? - if (mKnownTopics.find (lower) != mKnownTopics.end()) + if (mKnownTopics.count(lower)) { keywordList.push_back (iter->mId); } @@ -646,20 +636,20 @@ namespace MWDialogue { ESM::DialogueState state; - for (std::map::const_iterator iter (mKnownTopics.begin()); + for (std::set::const_iterator iter (mKnownTopics.begin()); iter!=mKnownTopics.end(); ++iter) - if (iter->second) - state.mKnownTopics.push_back (iter->first); + { + state.mKnownTopics.push_back (*iter); + } - state.mModFactionReaction = mModFactionReaction; + state.mChangedFactionReaction = mChangedFactionReaction; writer.startRecord (ESM::REC_DIAS); state.save (writer); writer.endRecord (ESM::REC_DIAS); - progress.increaseProgress(); } - void DialogueManager::readRecord (ESM::ESMReader& reader, int32_t type) + void DialogueManager::readRecord (ESM::ESMReader& reader, uint32_t type) { if (type==ESM::REC_DIAS) { @@ -671,9 +661,9 @@ namespace MWDialogue for (std::vector::const_iterator iter (state.mKnownTopics.begin()); iter!=state.mKnownTopics.end(); ++iter) if (store.get().search (*iter)) - mKnownTopics.insert (std::make_pair (*iter, true)); + mKnownTopics.insert (*iter); - mModFactionReaction = state.mModFactionReaction; + mChangedFactionReaction = state.mChangedFactionReaction; } } @@ -686,10 +676,23 @@ namespace MWDialogue MWBase::Environment::get().getWorld()->getStore().get().find(fact1); MWBase::Environment::get().getWorld()->getStore().get().find(fact2); - std::map& map = mModFactionReaction[fact1]; - if (map.find(fact2) == map.end()) - map[fact2] = 0; - map[fact2] += diff; + int newValue = getFactionReaction(faction1, faction2) + diff; + + std::map& map = mChangedFactionReaction[fact1]; + map[fact2] = newValue; + } + + void DialogueManager::setFactionReaction(const std::string &faction1, const std::string &faction2, int absolute) + { + std::string fact1 = Misc::StringUtils::lowerCase(faction1); + std::string fact2 = Misc::StringUtils::lowerCase(faction2); + + // Make sure the factions exist + MWBase::Environment::get().getWorld()->getStore().get().find(fact1); + MWBase::Environment::get().getWorld()->getStore().get().find(fact2); + + std::map& map = mChangedFactionReaction[fact1]; + map[fact2] = absolute; } int DialogueManager::getFactionReaction(const std::string &faction1, const std::string &faction2) const @@ -697,10 +700,9 @@ namespace MWDialogue std::string fact1 = Misc::StringUtils::lowerCase(faction1); std::string fact2 = Misc::StringUtils::lowerCase(faction2); - ModFactionReactionMap::const_iterator map = mModFactionReaction.find(fact1); - int diff = 0; - if (map != mModFactionReaction.end() && map->second.find(fact2) != map->second.end()) - diff = map->second.at(fact2); + ModFactionReactionMap::const_iterator map = mChangedFactionReaction.find(fact1); + if (map != mChangedFactionReaction.end() && map->second.find(fact2) != map->second.end()) + return map->second.at(fact2); const ESM::Faction* faction = MWBase::Environment::get().getWorld()->getStore().get().find(fact1); @@ -708,9 +710,9 @@ namespace MWDialogue for (; it != faction->mReactions.end(); ++it) { if (Misc::StringUtils::ciEqual(it->first, fact2)) - return it->second + diff; + return it->second; } - return diff; + return 0; } void DialogueManager::clearInfoActor(const MWWorld::Ptr &actor) const @@ -721,55 +723,4 @@ namespace MWDialogue mLastTopic, actor.getClass().getName(actor)); } } - - std::vector ParseHyperText(const std::string& text) - { - std::vector result; - MyGUI::UString utext(text); - size_t pos_begin, pos_end, iteration_pos = 0; - for(;;) - { - pos_begin = utext.find('@', iteration_pos); - if (pos_begin != std::string::npos) - pos_end = utext.find('#', pos_begin); - - if (pos_begin != std::string::npos && pos_end != std::string::npos) - { - result.push_back( HyperTextToken(utext.substr(iteration_pos, pos_begin - iteration_pos), false) ); - - std::string link = utext.substr(pos_begin + 1, pos_end - pos_begin - 1); - result.push_back( HyperTextToken(link, true) ); - - iteration_pos = pos_end + 1; - } - else - { - result.push_back( HyperTextToken(utext.substr(iteration_pos), false) ); - break; - } - } - - return result; - } - - size_t RemovePseudoAsterisks(std::string& phrase) - { - size_t pseudoAsterisksCount = 0; - const char specialPseudoAsteriskCharacter = 127; - - if( !phrase.empty() ) - { - std::string::reverse_iterator rit = phrase.rbegin(); - - while( rit != phrase.rend() && *rit == specialPseudoAsteriskCharacter ) - { - pseudoAsterisksCount++; - ++rit; - } - } - - phrase = phrase.substr(0, phrase.length() - pseudoAsterisksCount); - - return pseudoAsterisksCount; - } } diff --git a/apps/openmw/mwdialogue/dialoguemanagerimp.hpp b/apps/openmw/mwdialogue/dialoguemanagerimp.hpp index 6553ddc01..aec503e87 100644 --- a/apps/openmw/mwdialogue/dialoguemanagerimp.hpp +++ b/apps/openmw/mwdialogue/dialoguemanagerimp.hpp @@ -4,7 +4,7 @@ #include "../mwbase/dialoguemanager.hpp" #include -#include +#include #include #include @@ -23,13 +23,13 @@ namespace MWDialogue class DialogueManager : public MWBase::DialogueManager { std::map mDialogueMap; - std::map mKnownTopics;// Those are the topics the player knows. + std::set mKnownTopics;// Those are the topics the player knows. // Modified faction reactions. > typedef std::map > ModFactionReactionMap; - ModFactionReactionMap mModFactionReaction; + ModFactionReactionMap mChangedFactionReaction; - std::list mActorKnownTopics; + std::set mActorKnownTopics; Translation::Storage& mTranslationDataStorage; MWScript::CompilerContext mCompilerContext; @@ -92,32 +92,19 @@ namespace MWDialogue virtual void write (ESM::ESMWriter& writer, Loading::Listener& progress) const; - virtual void readRecord (ESM::ESMReader& reader, int32_t type); + virtual void readRecord (ESM::ESMReader& reader, uint32_t type); /// Changes faction1's opinion of faction2 by \a diff. virtual void modFactionReaction (const std::string& faction1, const std::string& faction2, int diff); + virtual void setFactionReaction (const std::string& faction1, const std::string& faction2, int absolute); + /// @return faction1's opinion of faction2 virtual int getFactionReaction (const std::string& faction1, const std::string& faction2) const; /// Removes the last added topic response for the given actor from the journal virtual void clearInfoActor (const MWWorld::Ptr& actor) const; }; - - - struct HyperTextToken - { - HyperTextToken(const std::string& text, bool link) : mText(text), mLink(link) {} - - std::string mText; - bool mLink; - }; - - // In translations (at least Russian) the links are marked with @#, so - // it should be a function to parse it - std::vector ParseHyperText(const std::string& text); - - size_t RemovePseudoAsterisks(std::string& phrase); } #endif diff --git a/apps/openmw/mwdialogue/filter.cpp b/apps/openmw/mwdialogue/filter.cpp index daa9a684a..adb7d3892 100644 --- a/apps/openmw/mwdialogue/filter.cpp +++ b/apps/openmw/mwdialogue/filter.cpp @@ -1,11 +1,14 @@ #include "filter.hpp" +#include + #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/journal.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/dialoguemanager.hpp" +#include "../mwbase/scriptmanager.hpp" #include "../mwworld/class.hpp" #include "../mwworld/inventorystore.hpp" @@ -64,14 +67,11 @@ bool MWDialogue::Filter::testActor (const ESM::DialInfo& info) const if (isCreature) return false; - MWMechanics::NpcStats& stats = mActor.getClass().getNpcStats (mActor); - std::map::iterator iter = stats.getFactionRanks().find ( Misc::StringUtils::lowerCase (info.mFaction)); - - if (iter==stats.getFactionRanks().end()) + if (!Misc::StringUtils::ciEqual(mActor.getClass().getPrimaryFaction(mActor), info.mFaction)) return false; // check rank - if (iter->second < info.mData.mRank) + if (mActor.getClass().getPrimaryFactionRank(mActor) < info.mData.mRank) return false; } else if (info.mData.mRank != -1) @@ -80,13 +80,8 @@ bool MWDialogue::Filter::testActor (const ESM::DialInfo& info) const return false; // Rank requirement, but no faction given. Use the actor's faction, if there is one. - MWMechanics::NpcStats& stats = mActor.getClass().getNpcStats (mActor); - - if (!stats.getFactionRanks().size()) - return false; - // check rank - if (stats.getFactionRanks().begin()->second < info.mData.mRank) + if (mActor.getClass().getPrimaryFactionRank(mActor) < info.mData.mRank) return false; } @@ -109,7 +104,7 @@ bool MWDialogue::Filter::testPlayer (const ESM::DialInfo& info) const if (!info.mPcFaction.empty()) { MWMechanics::NpcStats& stats = player.getClass().getNpcStats (player); - std::map::iterator iter = stats.getFactionRanks().find (Misc::StringUtils::lowerCase (info.mPcFaction)); + std::map::const_iterator iter = stats.getFactionRanks().find (Misc::StringUtils::lowerCase (info.mPcFaction)); if(iter==stats.getFactionRanks().end()) return false; @@ -123,7 +118,7 @@ bool MWDialogue::Filter::testPlayer (const ESM::DialInfo& info) const if (!info.mCell.empty()) { // supports partial matches, just like getPcCell - const std::string& playerCell = player.getCell()->getCell()->mName; + const std::string& playerCell = MWBase::Environment::get().getWorld()->getCellName(player.getCell()); bool match = playerCell.length()>=info.mCell.length() && Misc::StringUtils::ciEqual(playerCell.substr (0, info.mCell.length()), info.mCell); if (!match) @@ -197,33 +192,28 @@ bool MWDialogue::Filter::testSelectStructNumeric (const SelectWrapper& select) c if (scriptName.empty()) return false; // no script - const ESM::Script *script = - MWBase::Environment::get().getWorld()->getStore().get().find (scriptName); + std::string name = Misc::StringUtils::lowerCase (select.getName()); - std::string name = select.getName(); + const Compiler::Locals& localDefs = + MWBase::Environment::get().getScriptManager()->getLocals (scriptName); - int i = 0; + char type = localDefs.getType (name); - for (; i (script->mVarNames.size()); ++i) - if (Misc::StringUtils::ciEqual(script->mVarNames[i], name)) - break; + if (type==' ') + return false; // script does not have a variable of this name. - if (i>=static_cast (script->mVarNames.size())) - return false; // script does not have a variable of this name + int index = localDefs.getIndex (name); const MWScript::Locals& locals = mActor.getRefData().getLocals(); - if (imData.mNumShorts) - return select.selectCompare (static_cast (locals.mShorts[i])); - - i -= script->mData.mNumShorts; - - if (imData.mNumLongs) - return select.selectCompare (locals.mLongs[i]); - - i -= script->mData.mNumLongs; + switch (type) + { + case 's': return select.selectCompare (static_cast (locals.mShorts[index])); + case 'l': return select.selectCompare (locals.mLongs[index]); + case 'f': return select.selectCompare (locals.mFloats[index]); + } - return select.selectCompare (locals.mFloats.at (i)); + throw std::logic_error ("unknown local variable type in dialogue filter"); } case SelectWrapper::Function_PcHealthPercent: @@ -251,7 +241,7 @@ bool MWDialogue::Filter::testSelectStructNumeric (const SelectWrapper& select) c float ratio = mActor.getClass().getCreatureStats (mActor).getHealth().getCurrent() / mActor.getClass().getCreatureStats (mActor).getHealth().getModified(); - return select.selectCompare (ratio); + return select.selectCompare (static_cast(ratio*100)); } default: @@ -338,12 +328,10 @@ int MWDialogue::Filter::getSelectStructInteger (const SelectWrapper& select) con case SelectWrapper::Function_RankRequirement: { - if (mActor.getClass().getNpcStats (mActor).getFactionRanks().empty()) + std::string faction = mActor.getClass().getPrimaryFaction(mActor); + if (faction.empty()) return 0; - std::string faction = - mActor.getClass().getNpcStats (mActor).getFactionRanks().begin()->first; - int rank = getFactionRank (player, faction); if (rank>=9) @@ -378,15 +366,14 @@ int MWDialogue::Filter::getSelectStructInteger (const SelectWrapper& select) con case SelectWrapper::Function_FactionRankDiff: { - if (mActor.getClass().getNpcStats (mActor).getFactionRanks().empty()) - return 0; + std::string faction = mActor.getClass().getPrimaryFaction(mActor); - std::pair faction = - *mActor.getClass().getNpcStats (mActor).getFactionRanks().begin(); - - int rank = getFactionRank (player, faction.first); + if (faction.empty()) + return 0; - return rank-faction.second; + int rank = getFactionRank (player, faction); + int npcRank = mActor.getClass().getPrimaryFactionRank(mActor); + return rank-npcRank; } case SelectWrapper::Function_WerewolfKills: @@ -398,11 +385,10 @@ int MWDialogue::Filter::getSelectStructInteger (const SelectWrapper& select) con { bool low = select.getFunction()==SelectWrapper::Function_RankLow; - if (mActor.getClass().getNpcStats (mActor).getFactionRanks().empty()) - return 0; + std::string factionId = mActor.getClass().getPrimaryFaction(mActor); - std::string factionId = - mActor.getClass().getNpcStats (mActor).getFactionRanks().begin()->first; + if (factionId.empty()) + return 0; int value = 0; @@ -419,6 +405,21 @@ int MWDialogue::Filter::getSelectStructInteger (const SelectWrapper& select) con return value; } + case SelectWrapper::Function_CreatureTargetted: + + { + MWWorld::Ptr target; + mActor.getClass().getCreatureStats(mActor).getAiSequence().getCombatTarget(target); + if (target) + { + if (target.getClass().isNpc() && target.getClass().getNpcStats(target).isWerewolf()) + return 2; + if (target.getTypeName() == typeid(ESM::Creature).name()) + return 1; + } + } + return 0; + default: throw std::runtime_error ("unknown integer select function"); @@ -441,7 +442,7 @@ bool MWDialogue::Filter::getSelectStructBoolean (const SelectWrapper& select) co case SelectWrapper::Function_NotFaction: - return !Misc::StringUtils::ciEqual(mActor.get()->mBase->mFaction, select.getName()); + return !Misc::StringUtils::ciEqual(mActor.getClass().getPrimaryFaction(mActor), select.getName()); case SelectWrapper::Function_NotClass: @@ -453,7 +454,8 @@ bool MWDialogue::Filter::getSelectStructBoolean (const SelectWrapper& select) co case SelectWrapper::Function_NotCell: - return !Misc::StringUtils::ciEqual(mActor.getCell()->getCell()->mName, select.getName()); + return !Misc::StringUtils::ciEqual(MWBase::Environment::get().getWorld()->getCellName(mActor.getCell()) + , select.getName()); case SelectWrapper::Function_NotLocal: { @@ -463,20 +465,10 @@ bool MWDialogue::Filter::getSelectStructBoolean (const SelectWrapper& select) co // This actor has no attached script, so there is no local variable return true; - const ESM::Script *script = - MWBase::Environment::get().getWorld()->getStore().get().find (scriptName); - - std::string name = select.getName(); - - int i = 0; - for (; i < static_cast (script->mVarNames.size()); ++i) - if (Misc::StringUtils::ciEqual(script->mVarNames[i], name)) - break; - - if (i >= static_cast (script->mVarNames.size())) - return true; // script does not have a variable of this name + const Compiler::Locals& localDefs = + MWBase::Environment::get().getScriptManager()->getLocals (scriptName); - return false; + return localDefs.getIndex (Misc::StringUtils::lowerCase (select.getName()))==-1; } case SelectWrapper::Function_SameGender: @@ -490,8 +482,7 @@ bool MWDialogue::Filter::getSelectStructBoolean (const SelectWrapper& select) co case SelectWrapper::Function_SameFaction: - return mActor.getClass().getNpcStats (mActor).isSameFaction ( - player.getClass().getNpcStats (player)); + return player.getClass().getNpcStats (player).isInFaction(mActor.getClass().getPrimaryFaction(mActor)); case SelectWrapper::Function_PcCommonDisease: @@ -504,15 +495,14 @@ bool MWDialogue::Filter::getSelectStructBoolean (const SelectWrapper& select) co case SelectWrapper::Function_PcCorprus: return player.getClass().getCreatureStats (player). - getMagicEffects().get (ESM::MagicEffect::Corprus).mMagnitude!=0; + getMagicEffects().get (ESM::MagicEffect::Corprus).getMagnitude()!=0; case SelectWrapper::Function_PcExpelled: { - if (mActor.getClass().getNpcStats (mActor).getFactionRanks().empty()) - return false; + std::string faction = mActor.getClass().getPrimaryFaction(mActor); - std::string faction = - mActor.getClass().getNpcStats (mActor).getFactionRanks().begin()->first; + if (faction.empty()) + return false; return player.getClass().getNpcStats(player).getExpelled(faction); } @@ -520,7 +510,7 @@ bool MWDialogue::Filter::getSelectStructBoolean (const SelectWrapper& select) co case SelectWrapper::Function_PcVampire: return player.getClass().getCreatureStats(player).getMagicEffects(). - get(ESM::MagicEffect::Vampirism).mMagnitude > 0; + get(ESM::MagicEffect::Vampirism).getMagnitude() > 0; case SelectWrapper::Function_TalkedToPc: @@ -543,10 +533,6 @@ bool MWDialogue::Filter::getSelectStructBoolean (const SelectWrapper& select) co return MWBase::Environment::get().getMechanicsManager()->isAggressive(mActor, MWBase::Environment::get().getWorld()->getPlayerPtr()); - case SelectWrapper::Function_CreatureTargetted: - - return mActor.getClass().getCreatureStats (mActor).getCreatureTargetted(); - case SelectWrapper::Function_Werewolf: return mActor.getClass().getNpcStats (mActor).isWerewolf(); @@ -561,7 +547,7 @@ int MWDialogue::Filter::getFactionRank (const MWWorld::Ptr& actor, const std::st { MWMechanics::NpcStats& stats = actor.getClass().getNpcStats (actor); - std::map::const_iterator iter = stats.getFactionRanks().find (factionId); + std::map::const_iterator iter = stats.getFactionRanks().find (Misc::StringUtils::lowerCase(factionId)); if (iter==stats.getFactionRanks().end()) return -1; @@ -615,6 +601,17 @@ const ESM::DialInfo* MWDialogue::Filter::search (const ESM::Dialogue& dialogue, return suitableInfos[0]; } +std::vector MWDialogue::Filter::listAll (const ESM::Dialogue& dialogue) const +{ + std::vector infos; + for (ESM::Dialogue::InfoContainer::const_iterator iter = dialogue.mInfo.begin(); iter!=dialogue.mInfo.end(); ++iter) + { + if (testActor (*iter)) + infos.push_back(&*iter); + } + return infos; +} + std::vector MWDialogue::Filter::list (const ESM::Dialogue& dialogue, bool fallbackToInfoRefusal, bool searchAll, bool invertDisposition) const { diff --git a/apps/openmw/mwdialogue/filter.hpp b/apps/openmw/mwdialogue/filter.hpp index 7e7f2b6f5..d41b7aa1e 100644 --- a/apps/openmw/mwdialogue/filter.hpp +++ b/apps/openmw/mwdialogue/filter.hpp @@ -55,7 +55,11 @@ namespace MWDialogue std::vector list (const ESM::Dialogue& dialogue, bool fallbackToInfoRefusal, bool searchAll, bool invertDisposition=false) const; - ///< \note If fallbackToInfoRefusal is used, the returned DialInfo might not be from the supplied ESM::Dialogue. + ///< List all infos that could be used on the given actor, using the current runtime state of the actor. + /// \note If fallbackToInfoRefusal is used, the returned DialInfo might not be from the supplied ESM::Dialogue. + + std::vector listAll (const ESM::Dialogue& dialogue) const; + ///< List all infos that could possibly be used on the given actor, ignoring runtime state filters and ignoring player filters. const ESM::DialInfo* search (const ESM::Dialogue& dialogue, const bool fallbackToInfoRefusal) const; ///< Get a matching response for the requested dialogue. diff --git a/apps/openmw/mwdialogue/hypertextparser.cpp b/apps/openmw/mwdialogue/hypertextparser.cpp new file mode 100644 index 000000000..aa748e772 --- /dev/null +++ b/apps/openmw/mwdialogue/hypertextparser.cpp @@ -0,0 +1,93 @@ +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" + +#include "../mwworld/store.hpp" +#include "../mwworld/esmstore.hpp" + +#include "keywordsearch.hpp" + +#include "hypertextparser.hpp" + +namespace MWDialogue +{ + namespace HyperTextParser + { + std::vector parseHyperText(const std::string & text) + { + std::vector result; + size_t pos_end, iteration_pos = 0; + for(;;) + { + size_t pos_begin = text.find('@', iteration_pos); + if (pos_begin != std::string::npos) + pos_end = text.find('#', pos_begin); + + if (pos_begin != std::string::npos && pos_end != std::string::npos) + { + if (pos_begin != iteration_pos) + tokenizeKeywords(text.substr(iteration_pos, pos_begin - iteration_pos), result); + + std::string link = text.substr(pos_begin + 1, pos_end - pos_begin - 1); + result.push_back(Token(link, Token::ExplicitLink)); + + iteration_pos = pos_end + 1; + } + else + { + if (iteration_pos != text.size()) + tokenizeKeywords(text.substr(iteration_pos), result); + break; + } + } + + return result; + } + + void tokenizeKeywords(const std::string & text, std::vector & tokens) + { + const MWWorld::Store & dialogs = + MWBase::Environment::get().getWorld()->getStore().get(); + + std::list keywordList; + for (MWWorld::Store::iterator it = dialogs.begin(); it != dialogs.end(); ++it) + keywordList.push_back(Misc::StringUtils::lowerCase(it->mId)); + keywordList.sort(Misc::StringUtils::ciLess); + + KeywordSearch keywordSearch; + + for (std::list::const_iterator it = keywordList.begin(); it != keywordList.end(); ++it) + keywordSearch.seed(*it, 0 /*unused*/); + + std::vector::Match> matches; + keywordSearch.highlightKeywords(text.begin(), text.end(), matches); + + for (std::vector::Match>::const_iterator it = matches.begin(); it != matches.end(); ++it) + { + tokens.push_back(Token(std::string(it->mBeg, it->mEnd), Token::ImplicitKeyword)); + } + } + + size_t removePseudoAsterisks(std::string & phrase) + { + size_t pseudoAsterisksCount = 0; + + if( !phrase.empty() ) + { + std::string::reverse_iterator rit = phrase.rbegin(); + + const char specialPseudoAsteriskCharacter = 127; + while( rit != phrase.rend() && *rit == specialPseudoAsteriskCharacter ) + { + pseudoAsterisksCount++; + ++rit; + } + } + + phrase = phrase.substr(0, phrase.length() - pseudoAsterisksCount); + + return pseudoAsterisksCount; + } + } +} diff --git a/apps/openmw/mwdialogue/hypertextparser.hpp b/apps/openmw/mwdialogue/hypertextparser.hpp new file mode 100644 index 000000000..13e135f3c --- /dev/null +++ b/apps/openmw/mwdialogue/hypertextparser.hpp @@ -0,0 +1,36 @@ +#ifndef GAME_MWDIALOGUE_HYPERTEXTPARSER_H +#define GAME_MWDIALOGUE_HYPERTEXTPARSER_H + +#include +#include + +namespace MWDialogue +{ + namespace HyperTextParser + { + struct Token + { + enum Type + { + ExplicitLink, // enclosed in @# + ImplicitKeyword + }; + + Token(const std::string & text, Type type) : mText(text), mType(type) {} + + bool isExplicitLink() { return mType == ExplicitLink; } + bool isImplicitKeyword() { return mType == ImplicitKeyword; } + + std::string mText; + Type mType; + }; + + // In translations (at least Russian) the links are marked with @#, so + // it should be a function to parse it + std::vector parseHyperText(const std::string & text); + void tokenizeKeywords(const std::string & text, std::vector & tokens); + size_t removePseudoAsterisks(std::string & phrase); + } +} + +#endif diff --git a/apps/openmw/mwdialogue/journalentry.cpp b/apps/openmw/mwdialogue/journalentry.cpp index b92d7cace..9f07f7b6f 100644 --- a/apps/openmw/mwdialogue/journalentry.cpp +++ b/apps/openmw/mwdialogue/journalentry.cpp @@ -89,7 +89,7 @@ namespace MWDialogue for (ESM::Dialogue::InfoContainer::const_iterator iter (dialogue->mInfo.begin()); iter!=dialogue->mInfo.end(); ++iter) - if (iter->mData.mDisposition==index) /// \todo cleanup info structure + if (iter->mData.mJournalIndex==index) { return iter->mId; } diff --git a/apps/openmw/mwdialogue/journalimp.cpp b/apps/openmw/mwdialogue/journalimp.cpp index 9497347e3..99dab0cf8 100644 --- a/apps/openmw/mwdialogue/journalimp.cpp +++ b/apps/openmw/mwdialogue/journalimp.cpp @@ -92,9 +92,7 @@ namespace MWDialogue quest.addEntry (entry); // we are doing slicing on purpose here - std::vector empty; - std::string notification = "#{sJournalEntry}"; - MWBase::Environment::get().getWindowManager()->messageBox (notification, empty); + MWBase::Environment::get().getWindowManager()->messageBox ("#{sJournalEntry}"); } void Journal::setJournalIndex (const std::string& id, int index) @@ -189,7 +187,6 @@ namespace MWDialogue writer.startRecord (ESM::REC_QUES); state.save (writer); writer.endRecord (ESM::REC_QUES); - progress.increaseProgress(); for (Topic::TEntryIter iter (quest.begin()); iter!=quest.end(); ++iter) { @@ -200,7 +197,6 @@ namespace MWDialogue writer.startRecord (ESM::REC_JOUR); entry.save (writer); writer.endRecord (ESM::REC_JOUR); - progress.increaseProgress(); } } @@ -212,7 +208,6 @@ namespace MWDialogue writer.startRecord (ESM::REC_JOUR); entry.save (writer); writer.endRecord (ESM::REC_JOUR); - progress.increaseProgress(); } for (TTopicIter iter (mTopics.begin()); iter!=mTopics.end(); ++iter) @@ -228,14 +223,13 @@ namespace MWDialogue writer.startRecord (ESM::REC_JOUR); entry.save (writer); writer.endRecord (ESM::REC_JOUR); - progress.increaseProgress(); } } } - void Journal::readRecord (ESM::ESMReader& reader, int32_t type) + void Journal::readRecord (ESM::ESMReader& reader, uint32_t type) { - if (type==ESM::REC_JOUR) + if (type==ESM::REC_JOUR || type==ESM::REC_JOUR_LEGACY) { ESM::JournalEntry record; record.load (reader); @@ -265,7 +259,12 @@ namespace MWDialogue record.load (reader); if (isThere (record.mTopic)) - mQuests.insert (std::make_pair (record.mTopic, record)); + { + std::pair result = mQuests.insert (std::make_pair (record.mTopic, record)); + // reapply quest index, this is to handle users upgrading from only + // Morrowind.esm (no quest states) to Morrowind.esm + Tribunal.esm + result.first->second.setIndex(record.mState); + } } } } diff --git a/apps/openmw/mwdialogue/journalimp.hpp b/apps/openmw/mwdialogue/journalimp.hpp index d15b909fa..7f26a5bb9 100644 --- a/apps/openmw/mwdialogue/journalimp.hpp +++ b/apps/openmw/mwdialogue/journalimp.hpp @@ -71,7 +71,7 @@ namespace MWDialogue virtual void write (ESM::ESMWriter& writer, Loading::Listener& progress) const; - virtual void readRecord (ESM::ESMReader& reader, int32_t type); + virtual void readRecord (ESM::ESMReader& reader, uint32_t type); }; } diff --git a/apps/openmw/mwdialogue/keywordsearch.cpp b/apps/openmw/mwdialogue/keywordsearch.cpp new file mode 100644 index 000000000..e69de29bb diff --git a/apps/openmw/mwgui/keywordsearch.hpp b/apps/openmw/mwdialogue/keywordsearch.hpp similarity index 73% rename from apps/openmw/mwgui/keywordsearch.hpp rename to apps/openmw/mwdialogue/keywordsearch.hpp index 4f4459b35..c4e1d7553 100644 --- a/apps/openmw/mwgui/keywordsearch.hpp +++ b/apps/openmw/mwdialogue/keywordsearch.hpp @@ -1,5 +1,5 @@ -#ifndef MWGUI_KEYWORDSEARCH_H -#define MWGUI_KEYWORDSEARCH_H +#ifndef GAME_MWDIALOGUE_KEYWORDSEARCH_H +#define GAME_MWDIALOGUE_KEYWORDSEARCH_H #include #include @@ -9,7 +9,7 @@ #include -namespace MWGui +namespace MWDialogue { template @@ -28,6 +28,8 @@ public: void seed (string_t keyword, value_t value) { + if (keyword.empty()) + return; seed_impl (/*std::move*/ (keyword), /*std::move*/ (value), 0, mRoot); } @@ -66,19 +68,26 @@ public: return false; } - bool search (Point beg, Point end, Match & match, Point start) + static bool sortMatches(const Match& left, const Match& right) { + return left.mBeg < right.mBeg; + } + + void highlightKeywords (Point beg, Point end, std::vector& out) + { + std::vector matches; for (Point i = beg; i != end; ++i) { // check if previous character marked start of new word - if (i != start) + if (i != beg) { Point prev = i; - --prev; + --prev; if(isalpha(*prev)) continue; } + // check first character typename Entry::childen_t::iterator candidate = mRoot.mChildren.find (std::tolower (*i, mLocale)); @@ -137,17 +146,57 @@ public: if (t != candidate->second.mKeyword.end ()) continue; - // we did it, report the good news + // found a keyword, but there might still be longer keywords that start somewhere _within_ this keyword + // we will resolve these overlapping keywords later, choosing the longest one in case of conflict + Match match; match.mValue = candidate->second.mValue; match.mBeg = i; match.mEnd = k; + matches.push_back(match); + break; + } + } - return true; + // resolve overlapping keywords + while (matches.size()) + { + int longestKeywordSize = 0; + typename std::vector::iterator longestKeyword = matches.begin(); + for (typename std::vector::iterator it = matches.begin(); it != matches.end(); ++it) + { + int size = it->mEnd - it->mBeg; + if (size > longestKeywordSize) + { + longestKeywordSize = size; + longestKeyword = it; + } + + typename std::vector::iterator next = it; + ++next; + + if (next == matches.end()) + break; + + if (it->mEnd <= next->mBeg) + { + break; // no overlap + } + } + + Match keyword = *longestKeyword; + matches.erase(longestKeyword); + out.push_back(keyword); + // erase anything that overlaps with the keyword we just added to the output + for (typename std::vector::iterator it = matches.begin(); it != matches.end();) + { + if (it->mBeg < keyword.mEnd && it->mEnd > keyword.mBeg) + it = matches.erase(it); + else + ++it; } } - // no match in range, report the bad news - return false; + std::sort(out.begin(), out.end(), sortMatches); } private: diff --git a/apps/openmw/mwdialogue/quest.cpp b/apps/openmw/mwdialogue/quest.cpp index a699286a1..a9e39b379 100644 --- a/apps/openmw/mwdialogue/quest.cpp +++ b/apps/openmw/mwdialogue/quest.cpp @@ -75,7 +75,7 @@ namespace MWDialogue iter!=dialogue->mInfo.end(); ++iter) if (iter->mId == entry.mInfoId) { - index = iter->mData.mDisposition; /// \todo cleanup info structure + index = iter->mData.mJournalIndex; break; } diff --git a/apps/openmw/mwdialogue/scripttest.cpp b/apps/openmw/mwdialogue/scripttest.cpp new file mode 100644 index 000000000..3f4654610 --- /dev/null +++ b/apps/openmw/mwdialogue/scripttest.cpp @@ -0,0 +1,128 @@ +#include "scripttest.hpp" + +#include + +#include "../mwworld/manualref.hpp" +#include "../mwworld/esmstore.hpp" +#include "../mwworld/class.hpp" + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" +#include "../mwbase/scriptmanager.hpp" + +#include "../mwscript/compilercontext.hpp" + +#include +#include +#include +#include +#include +#include + +#include "filter.hpp" + +namespace +{ + +void test(const MWWorld::Ptr& actor, int &compiled, int &total, const Compiler::Extensions* extensions, int warningsMode) +{ + MWDialogue::Filter filter(actor, 0, false); + + MWScript::CompilerContext compilerContext(MWScript::CompilerContext::Type_Dialogue); + compilerContext.setExtensions(extensions); + std::ostream errorStream(std::cout.rdbuf()); + Compiler::StreamErrorHandler errorHandler(errorStream); + errorHandler.setWarningsMode (warningsMode); + + const MWWorld::Store& dialogues = MWBase::Environment::get().getWorld()->getStore().get(); + for (MWWorld::Store::iterator it = dialogues.begin(); it != dialogues.end(); ++it) + { + std::vector infos = filter.listAll(*it); + + for (std::vector::iterator it = infos.begin(); it != infos.end(); ++it) + { + const ESM::DialInfo* info = *it; + if (!info->mResultScript.empty()) + { + bool success = true; + ++total; + try + { + errorHandler.reset(); + + std::istringstream input (info->mResultScript + "\n"); + + Compiler::Scanner scanner (errorHandler, input, extensions); + + Compiler::Locals locals; + + std::string actorScript = actor.getClass().getScript(actor); + + if (!actorScript.empty()) + { + // grab local variables from actor's script, if available. + locals = MWBase::Environment::get().getScriptManager()->getLocals (actorScript); + } + + Compiler::ScriptParser parser(errorHandler, compilerContext, locals, false); + + scanner.scan (parser); + + if (!errorHandler.isGood()) + success = false; + + ++compiled; + } + catch (const Compiler::SourceException& /* error */) + { + // error has already been reported via error handler + success = false; + } + catch (const std::exception& error) + { + std::cerr << std::string ("Dialogue error: An exception has been thrown: ") + error.what() << std::endl; + success = false; + } + + if (!success) + { + std::cerr + << "compiling failed (dialogue script)" << std::endl + << info->mResultScript + << std::endl << std::endl; + } + } + } + } +} + +} + +namespace MWDialogue +{ + +namespace ScriptTest +{ + + std::pair compileAll(const Compiler::Extensions *extensions, int warningsMode) + { + int compiled = 0, total = 0; + const MWWorld::Store& npcs = MWBase::Environment::get().getWorld()->getStore().get(); + for (MWWorld::Store::iterator it = npcs.begin(); it != npcs.end(); ++it) + { + MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), it->mId); + test(ref.getPtr(), compiled, total, extensions, warningsMode); + } + + const MWWorld::Store& creatures = MWBase::Environment::get().getWorld()->getStore().get(); + for (MWWorld::Store::iterator it = creatures.begin(); it != creatures.end(); ++it) + { + MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), it->mId); + test(ref.getPtr(), compiled, total, extensions, warningsMode); + } + return std::make_pair(total, compiled); + } + +} + +} diff --git a/apps/openmw/mwdialogue/scripttest.hpp b/apps/openmw/mwdialogue/scripttest.hpp new file mode 100644 index 000000000..0ac259725 --- /dev/null +++ b/apps/openmw/mwdialogue/scripttest.hpp @@ -0,0 +1,20 @@ +#ifndef OPENMW_MWDIALOGUE_SCRIPTTEST_H +#define OPENMW_MWDIALOGUE_SCRIPTTEST_H + +#include + +namespace MWDialogue +{ + +namespace ScriptTest +{ + +/// Attempt to compile all dialogue scripts, use for verification purposes +/// @return A pair containing +std::pair compileAll(const Compiler::Extensions* extensions, int warningsMode); + +} + +} + +#endif diff --git a/apps/openmw/mwdialogue/selectwrapper.cpp b/apps/openmw/mwdialogue/selectwrapper.cpp index 3f22998f0..fa0fbfe13 100644 --- a/apps/openmw/mwdialogue/selectwrapper.cpp +++ b/apps/openmw/mwdialogue/selectwrapper.cpp @@ -205,6 +205,7 @@ MWDialogue::SelectWrapper::Type MWDialogue::SelectWrapper::getType() const Function_Reputation, Function_FactionRankDiff, Function_WerewolfKills, Function_RankLow, Function_RankHigh, + Function_CreatureTargetted, Function_None // end marker }; @@ -225,7 +226,6 @@ MWDialogue::SelectWrapper::Type MWDialogue::SelectWrapper::getType() const Function_PcVampire, Function_TalkedToPc, Function_Alarmed, Function_Detected, Function_Attacked, Function_ShouldAttack, - Function_CreatureTargetted, Function_Werewolf, Function_None // end marker }; diff --git a/apps/openmw/mwgui/alchemywindow.cpp b/apps/openmw/mwgui/alchemywindow.cpp index 0b3e3c168..b28e4de09 100644 --- a/apps/openmw/mwgui/alchemywindow.cpp +++ b/apps/openmw/mwgui/alchemywindow.cpp @@ -1,14 +1,21 @@ #include "alchemywindow.hpp" #include -#include + +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwmechanics/magiceffects.hpp" +#include "../mwmechanics/alchemy.hpp" + #include "../mwworld/class.hpp" +#include "../mwworld/esmstore.hpp" + +#include #include "inventoryitemmodel.hpp" #include "sortfilteritemmodel.hpp" @@ -22,6 +29,7 @@ namespace MWGui , mApparatus (4) , mIngredients (4) , mSortModel(NULL) + , mAlchemy(new MWMechanics::Alchemy()) { getWidget(mCreateButton, "CreateButton"); getWidget(mCancelButton, "CancelButton"); @@ -61,7 +69,7 @@ namespace MWGui std::string name = mNameEdit->getCaption(); boost::algorithm::trim(name); - MWMechanics::Alchemy::Result result = mAlchemy.create (mNameEdit->getCaption ()); + MWMechanics::Alchemy::Result result = mAlchemy->create (mNameEdit->getCaption ()); if (result == MWMechanics::Alchemy::Result_NoName) { @@ -116,7 +124,7 @@ namespace MWGui void AlchemyWindow::open() { - mAlchemy.setAlchemist (MWBase::Environment::get().getWorld()->getPlayerPtr()); + mAlchemy->setAlchemist (MWBase::Environment::get().getWorld()->getPlayerPtr()); InventoryItemModel* model = new InventoryItemModel(MWBase::Environment::get().getWorld()->getPlayerPtr()); mSortModel = new SortFilterItemModel(model); @@ -127,16 +135,17 @@ namespace MWGui int index = 0; - mAlchemy.setAlchemist (MWBase::Environment::get().getWorld()->getPlayerPtr()); + mAlchemy->setAlchemist (MWBase::Environment::get().getWorld()->getPlayerPtr()); - for (MWMechanics::Alchemy::TToolsIterator iter (mAlchemy.beginTools()); - iter!=mAlchemy.endTools() && index (mApparatus.size()); ++iter, ++index) + for (MWMechanics::Alchemy::TToolsIterator iter (mAlchemy->beginTools()); + iter!=mAlchemy->endTools() && index (mApparatus.size()); ++iter, ++index) { + mApparatus.at (index)->setItem(*iter); + mApparatus.at (index)->clearUserStrings(); if (!iter->isEmpty()) { mApparatus.at (index)->setUserString ("ToolTipType", "ItemPtr"); mApparatus.at (index)->setUserData (*iter); - mApparatus.at (index)->setItem(*iter); } } @@ -144,7 +153,7 @@ namespace MWGui } void AlchemyWindow::exit() { - mAlchemy.clear(); + mAlchemy->clear(); MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Alchemy); MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Inventory); } @@ -158,7 +167,7 @@ namespace MWGui void AlchemyWindow::onSelectedItem(int index) { MWWorld::Ptr item = mSortModel->getItem(index).mBase; - int res = mAlchemy.addIngredient(item); + int res = mAlchemy->addIngredient(item); if (res != -1) { @@ -171,15 +180,20 @@ namespace MWGui void AlchemyWindow::update() { + std::string suggestedName = mAlchemy->suggestPotionName(); + if (suggestedName != mSuggestedPotionName) + mNameEdit->setCaptionWithReplacing(suggestedName); + mSuggestedPotionName = suggestedName; + mSortModel->clearDragItems(); - MWMechanics::Alchemy::TIngredientsIterator it = mAlchemy.beginIngredients (); + MWMechanics::Alchemy::TIngredientsIterator it = mAlchemy->beginIngredients (); for (int i=0; i<4; ++i) { ItemWidget* ingredient = mIngredients[i]; MWWorld::Ptr item; - if (it != mAlchemy.endIngredients ()) + if (it != mAlchemy->endIngredients ()) { item = *it; ++it; @@ -201,22 +215,26 @@ namespace MWGui ingredient->setUserString("ToolTipType", "ItemPtr"); ingredient->setUserData(item); - MyGUI::TextBox* text = ingredient->createWidget("SandBrightText", MyGUI::IntCoord(0, 14, 32, 18), MyGUI::Align::Default, std::string("Label")); - text->setTextAlign(MyGUI::Align::Right); - text->setNeedMouseFocus(false); - text->setTextShadow(true); - text->setTextShadowColour(MyGUI::Colour(0,0,0)); - text->setCaption(ItemView::getCountString(ingredient->getUserData()->getRefData().getCount())); + ingredient->setCount(ingredient->getUserData()->getRefData().getCount()); } mItemView->update(); - std::vector effects; - ESM::EffectList list; - list.mList = effects; - for (MWMechanics::Alchemy::TEffectsIterator it = mAlchemy.beginEffects (); it != mAlchemy.endEffects (); ++it) + std::set effectIds = mAlchemy->listEffects(); + Widgets::SpellEffectList list; + for (std::set::iterator it = effectIds.begin(); it != effectIds.end(); ++it) { - list.mList.push_back(*it); + Widgets::SpellEffectParams params; + params.mEffectID = it->mId; + const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find(it->mId); + if (magicEffect->mData.mFlags & ESM::MagicEffect::TargetSkill) + params.mSkill = it->mArg; + else if (magicEffect->mData.mFlags & ESM::MagicEffect::TargetAttribute) + params.mAttribute = it->mArg; + params.mIsConstant = true; + params.mNoTarget = true; + + list.push_back(params); } while (mEffectsBox->getChildCount()) @@ -226,8 +244,7 @@ namespace MWGui Widgets::MWEffectListPtr effectsWidget = mEffectsBox->createWidget ("MW_StatName", coord, MyGUI::Align::Left | MyGUI::Align::Top); - Widgets::SpellEffectList _list = Widgets::MWEffectList::effectListFromESM(&list); - effectsWidget->setEffectList(_list); + effectsWidget->setEffectList(list); std::vector effectItems; effectsWidget->createEffectWidgets(effectItems, mEffectsBox, coord, false, 0); @@ -238,7 +255,7 @@ namespace MWGui { for (int i=0; i<4; ++i) if (mIngredients[i] == ingredient) - mAlchemy.removeIngredient (i); + mAlchemy->removeIngredient (i); update(); } diff --git a/apps/openmw/mwgui/alchemywindow.hpp b/apps/openmw/mwgui/alchemywindow.hpp index b538a1f80..69fff8c67 100644 --- a/apps/openmw/mwgui/alchemywindow.hpp +++ b/apps/openmw/mwgui/alchemywindow.hpp @@ -3,11 +3,14 @@ #include -#include "../mwmechanics/alchemy.hpp" - #include "widgets.hpp" #include "windowbase.hpp" +namespace MWMechanics +{ + class Alchemy; +} + namespace MWGui { class ItemView; @@ -23,6 +26,8 @@ namespace MWGui virtual void exit(); private: + std::string mSuggestedPotionName; + ItemView* mItemView; SortFilterItemModel* mSortModel; @@ -43,7 +48,7 @@ namespace MWGui void update(); - MWMechanics::Alchemy mAlchemy; + std::auto_ptr mAlchemy; std::vector mApparatus; std::vector mIngredients; diff --git a/apps/openmw/mwgui/backgroundimage.cpp b/apps/openmw/mwgui/backgroundimage.cpp index 1e87c0ff1..9c07c5780 100644 --- a/apps/openmw/mwgui/backgroundimage.cpp +++ b/apps/openmw/mwgui/backgroundimage.cpp @@ -5,14 +5,14 @@ namespace MWGui { -void BackgroundImage::setBackgroundImage (const std::string& image, bool fixedRatio, bool correct) +void BackgroundImage::setBackgroundImage (const std::string& image, bool fixedRatio, bool stretch) { if (mChild) { MyGUI::Gui::getInstance().destroyWidget(mChild); mChild = NULL; } - if (correct) + if (!stretch) { setImageTexture("black.png"); diff --git a/apps/openmw/mwgui/backgroundimage.hpp b/apps/openmw/mwgui/backgroundimage.hpp index 3d1a61eaf..8c963b762 100644 --- a/apps/openmw/mwgui/backgroundimage.hpp +++ b/apps/openmw/mwgui/backgroundimage.hpp @@ -18,9 +18,9 @@ namespace MWGui /** * @param fixedRatio Use a fixed ratio of 4:3, regardless of the image dimensions - * @param correct Add black bars? + * @param stretch Stretch to fill the whole screen, or add black bars? */ - void setBackgroundImage (const std::string& image, bool fixedRatio=true, bool correct=true); + void setBackgroundImage (const std::string& image, bool fixedRatio=true, bool stretch=true); virtual void setSize (const MyGUI::IntSize &_value); virtual void setCoord (const MyGUI::IntCoord &_value); diff --git a/apps/openmw/mwgui/birth.cpp b/apps/openmw/mwgui/birth.cpp index 7f58309ba..668253712 100644 --- a/apps/openmw/mwgui/birth.cpp +++ b/apps/openmw/mwgui/birth.cpp @@ -1,10 +1,16 @@ #include "birth.hpp" -#include +#include +#include +#include + +#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwworld/esmstore.hpp" #include "widgets.hpp" @@ -78,8 +84,6 @@ namespace MWGui if (Misc::StringUtils::ciEqual(*mBirthList->getItemDataAt(i), birthId)) { mBirthList->setIndexSelected(i); - MyGUI::Button* okButton; - getWidget(okButton, "OKButton"); break; } } @@ -114,9 +118,6 @@ namespace MWGui if (_index == MyGUI::ITEM_NONE) return; - MyGUI::Button* okButton; - getWidget(okButton, "OKButton"); - const std::string *birthId = mBirthList->getItemDataAt(_index); if (Misc::StringUtils::ciEqual(mCurrentBirthId, *birthId)) return; @@ -182,9 +183,7 @@ namespace MWGui const ESM::BirthSign *birth = store.get().find(mCurrentBirthId); - std::string texturePath = std::string("textures\\") + birth->mTexture; - Widgets::fixTexturePath(texturePath); - mBirthImage->setImageTexture(texturePath); + mBirthImage->setImageTexture(Misc::ResourceHelpers::correctTexturePath(birth->mTexture)); std::vector abilities, powers, spells; @@ -233,7 +232,7 @@ namespace MWGui for (std::vector::const_iterator it = categories[category].spells.begin(); it != end; ++it) { const std::string &spellId = *it; - spellWidget = mSpellArea->createWidget("MW_StatName", coord, MyGUI::Align::Default, std::string("Spell") + boost::lexical_cast(i)); + spellWidget = mSpellArea->createWidget("MW_StatName", coord, MyGUI::Align::Default, std::string("Spell") + MyGUI::utility::toString(i)); spellWidget->setSpellId(spellId); mSpellItems.push_back(spellWidget); diff --git a/apps/openmw/mwgui/birth.hpp b/apps/openmw/mwgui/birth.hpp index 20a64c78c..0a84bb4e9 100644 --- a/apps/openmw/mwgui/birth.hpp +++ b/apps/openmw/mwgui/birth.hpp @@ -3,11 +3,6 @@ #include "windowbase.hpp" -/* - This file contains the dialog for choosing a birth sign. - Layout is defined by resources/mygui/openmw_chargen_race.layout. - */ - namespace MWGui { class BirthDialog : public WindowModal @@ -35,6 +30,11 @@ namespace MWGui */ EventHandle_Void eventBack; + /** Event : Dialog finished, OK button clicked.\n + signature : void method()\n + */ + EventHandle_WindowBase eventDone; + protected: void onSelectBirth(MyGUI::ListBox* _sender, size_t _index); diff --git a/apps/openmw/mwgui/bookpage.cpp b/apps/openmw/mwgui/bookpage.cpp index 659ba714c..c9cfc8c2c 100644 --- a/apps/openmw/mwgui/bookpage.cpp +++ b/apps/openmw/mwgui/bookpage.cpp @@ -195,8 +195,20 @@ struct TypesetBookImpl : TypesetBook struct TypesetBookImpl::Typesetter : BookTypesetter { + struct PartialText { + StyleImpl *mStyle; + Utf8Stream::Point mBegin; + Utf8Stream::Point mEnd; + int mWidth; + + PartialText( StyleImpl *style, Utf8Stream::Point begin, Utf8Stream::Point end, int width) : + mStyle(style), mBegin(begin), mEnd(end), mWidth(width) + {} + }; + typedef TypesetBookImpl Book; typedef boost::shared_ptr BookPtr; + typedef std::vector::const_iterator PartialTextConstIterator; int mPageWidth; int mPageHeight; @@ -207,6 +219,8 @@ struct TypesetBookImpl::Typesetter : BookTypesetter Run * mRun; std::vector mSectionAlignment; + std::vector mPartialWhitespace; + std::vector mPartialWord; Book::Content const * mCurrentContent; Alignment mCurrentAlignment; @@ -246,7 +260,7 @@ struct TypesetBookImpl::Typesetter : BookTypesetter Style* createHotStyle (Style* baseStyle, Colour normalColour, Colour hoverColour, Colour activeColour, InteractiveId id, bool unique) { - StyleImpl* BaseStyle = dynamic_cast (baseStyle); + StyleImpl* BaseStyle = static_cast (baseStyle); if (!unique) for (Styles::iterator i = mBook->mStyles.begin (); i != mBook->mStyles.end (); ++i) @@ -268,11 +282,13 @@ struct TypesetBookImpl::Typesetter : BookTypesetter { Range range = mBook->addContent (text); - writeImpl (dynamic_cast (style), range.first, range.second); + writeImpl (static_cast (style), range.first, range.second); } intptr_t addContent (Utf8Span text, bool select) { + add_partial_text(); + Contents::iterator i = mBook->mContents.insert (mBook->mContents.end (), Content (text.first, text.second)); if (select) @@ -283,6 +299,8 @@ struct TypesetBookImpl::Typesetter : BookTypesetter void selectContent (intptr_t contentHandle) { + add_partial_text(); + mCurrentContent = reinterpret_cast (contentHandle); } @@ -295,19 +313,23 @@ struct TypesetBookImpl::Typesetter : BookTypesetter Utf8Point begin_ = &mCurrentContent->front () + begin; Utf8Point end_ = &mCurrentContent->front () + end ; - writeImpl (dynamic_cast (style), begin_, end_); + writeImpl (static_cast (style), begin_, end_); } void lineBreak (float margin) { assert (margin == 0); //TODO: figure out proper behavior here... + add_partial_text(); + mRun = NULL; mLine = NULL; } void sectionBreak (float margin) { + add_partial_text(); + if (mBook->mSections.size () > 0) { mRun = NULL; @@ -321,6 +343,8 @@ struct TypesetBookImpl::Typesetter : BookTypesetter void setSectionAlignment (Alignment sectionAlignment) { + add_partial_text(); + if (mSection != NULL) mSectionAlignment.back () = sectionAlignment; mCurrentAlignment = sectionAlignment; @@ -331,6 +355,8 @@ struct TypesetBookImpl::Typesetter : BookTypesetter int curPageStart = 0; int curPageStop = 0; + add_partial_text(); + std::vector ::iterator sa = mSectionAlignment.begin (); for (Sections::iterator i = mBook->mSections.begin (); i != mBook->mSections.end (); ++i, ++sa) { @@ -386,8 +412,6 @@ struct TypesetBookImpl::Typesetter : BookTypesetter int sectionHeightLeft = sectionHeight; while (sectionHeightLeft > mPageHeight) { - spaceLeft = mPageHeight - (curPageStop - curPageStart); - // Adjust to the top of the first line that does not fit on the current page anymore int splitPos = curPageStop; for (Lines::iterator j = i->mLines.begin (); j != i->mLines.end (); ++j) @@ -417,23 +441,23 @@ struct TypesetBookImpl::Typesetter : BookTypesetter void writeImpl (StyleImpl * style, Utf8Stream::Point _begin, Utf8Stream::Point _end) { - int line_height = style->mFont->getDefaultHeight (); - Utf8Stream stream (_begin, _end); while (!stream.eof ()) { if (ucsLineBreak (stream.peek ())) { + add_partial_text(); stream.consume (); mLine = NULL, mRun = NULL; continue; } + if (ucsBreakingSpace (stream.peek ()) && !mPartialWord.empty()) + add_partial_text(); + int word_width = 0; - int word_height = 0; int space_width = 0; - int character_count = 0; Utf8Stream::Point lead = stream.current (); @@ -452,8 +476,6 @@ struct TypesetBookImpl::Typesetter : BookTypesetter MyGUI::GlyphInfo* gi = style->mFont->getGlyphInfo (stream.peek ()); if (gi) word_width += gi->advance + gi->bearingX; - word_height = line_height; - ++character_count; stream.consume (); } @@ -462,21 +484,57 @@ struct TypesetBookImpl::Typesetter : BookTypesetter if (lead == extent) break; - int left = mLine ? mLine->mRect.right : 0; + if ( lead != origin ) + mPartialWhitespace.push_back (PartialText (style, lead, origin, space_width)); + if ( origin != extent ) + mPartialWord.push_back (PartialText (style, origin, extent, word_width)); + } + } - if (left + space_width + word_width > mPageWidth) - { - mLine = NULL, mRun = NULL; + void add_partial_text () + { + if (mPartialWhitespace.empty() && mPartialWord.empty()) + return; - append_run (style, origin, extent, extent - origin, word_width, mBook->mRect.bottom + word_height); - } - else + int space_width = 0; + int word_width = 0; + + for (PartialTextConstIterator i = mPartialWhitespace.begin (); i != mPartialWhitespace.end (); ++i) + space_width += i->mWidth; + for (PartialTextConstIterator i = mPartialWord.begin (); i != mPartialWord.end (); ++i) + word_width += i->mWidth; + + int left = mLine ? mLine->mRect.right : 0; + + if (left + space_width + word_width > mPageWidth) + { + mLine = NULL, mRun = NULL, left = 0; + } + else + { + for (PartialTextConstIterator i = mPartialWhitespace.begin (); i != mPartialWhitespace.end (); ++i) { int top = mLine ? mLine->mRect.top : mBook->mRect.bottom; + int line_height = i->mStyle->mFont->getDefaultHeight (); + + append_run ( i->mStyle, i->mBegin, i->mEnd, 0, left + i->mWidth, top + line_height); - append_run (style, lead, extent, extent - origin, left + space_width + word_width, top + word_height); + left = mLine->mRect.right; } } + + for (PartialTextConstIterator i = mPartialWord.begin (); i != mPartialWord.end (); ++i) + { + int top = mLine ? mLine->mRect.top : mBook->mRect.bottom; + int line_height = i->mStyle->mFont->getDefaultHeight (); + + append_run (i->mStyle, i->mBegin, i->mEnd, i->mEnd - i->mBegin, left + i->mWidth, top + line_height); + + left = mLine->mRect.right; + } + + mPartialWhitespace.clear(); + mPartialWord.clear(); } void append_run (StyleImpl * style, Utf8Stream::Point begin, Utf8Stream::Point end, int pc, int right, int bottom) @@ -641,7 +699,8 @@ namespace MyGUI::Vertex* vertices, RenderXform const & renderXform) : mZ(Z), mOrigin (left, top), mFont (font), mVertices (vertices), - mRenderXform (renderXform) + mRenderXform (renderXform), + mC(0) { mVertexColourType = MyGUI::RenderManager::getInstance().getVertexFormat(); } diff --git a/apps/openmw/mwgui/bookwindow.cpp b/apps/openmw/mwgui/bookwindow.cpp index 32a5255c9..da0d1950e 100644 --- a/apps/openmw/mwgui/bookwindow.cpp +++ b/apps/openmw/mwgui/bookwindow.cpp @@ -1,6 +1,6 @@ #include "bookwindow.hpp" -#include +#include #include @@ -70,11 +70,6 @@ namespace MWGui void BookWindow::clearPages() { - for (std::vector::iterator it=mPages.begin(); - it!=mPages.end(); ++it) - { - MyGUI::Gui::getInstance().destroyWidget(*it); - } mPages.clear(); } @@ -89,25 +84,9 @@ namespace MWGui MWWorld::LiveCellRef *ref = mBook.get(); - BookTextParser parser; - std::vector results = parser.split(ref->mBase->mText, mLeftPage->getSize().width, mLeftPage->getSize().height); - - int i=0; - for (std::vector::iterator it=results.begin(); - it!=results.end(); ++it) - { - MyGUI::Widget* parent; - if (i%2 == 0) - parent = mLeftPage; - else - parent = mRightPage; - - MyGUI::Widget* pageWidget = parent->createWidgetReal("", MyGUI::FloatCoord(0.0,0.0,1.0,1.0), MyGUI::Align::Default, "BookPage" + boost::lexical_cast(i)); - pageWidget->setNeedMouseFocus(false); - parser.parsePage(*it, pageWidget, mLeftPage->getSize().width); - mPages.push_back(pageWidget); - ++i; - } + Formatting::BookFormatter formatter; + mPages = formatter.markupToWidget(mLeftPage, ref->mBase->mText); + formatter.markupToWidget(mRightPage, ref->mBase->mText); updatePages(); @@ -161,21 +140,8 @@ namespace MWGui void BookWindow::updatePages() { - mLeftPageNumber->setCaption( boost::lexical_cast(mCurrentPage*2 + 1) ); - mRightPageNumber->setCaption( boost::lexical_cast(mCurrentPage*2 + 2) ); - - unsigned int i=0; - for (std::vector::iterator it = mPages.begin(); - it != mPages.end(); ++it) - { - if (mCurrentPage*2 == i || mCurrentPage*2+1 == i) - (*it)->setVisible(true); - else - { - (*it)->setVisible(false); - } - ++i; - } + mLeftPageNumber->setCaption( MyGUI::utility::toString(mCurrentPage*2 + 1) ); + mRightPageNumber->setCaption( MyGUI::utility::toString(mCurrentPage*2 + 2) ); //If it is the last page, hide the button "Next Page" if ( (mCurrentPage+1)*2 == mPages.size() @@ -191,9 +157,30 @@ namespace MWGui } else { mPrevPageButton->setVisible(true); } + + if (mPages.empty()) + return; + + MyGUI::Widget * paper; + + paper = mLeftPage->getChildAt(0); + paper->setCoord(paper->getPosition().left, -mPages[mCurrentPage*2].first, + paper->getWidth(), mPages[mCurrentPage*2].second); + + paper = mRightPage->getChildAt(0); + if ((mCurrentPage+1)*2 <= mPages.size()) + { + paper->setCoord(paper->getPosition().left, -mPages[mCurrentPage*2+1].first, + paper->getWidth(), mPages[mCurrentPage*2+1].second); + paper->setVisible(true); + } + else + { + paper->setVisible(false); + } } - void BookWindow::adjustButton (MWGui::ImageButton* button) + void BookWindow::adjustButton (Gui::ImageButton* button) { MyGUI::IntSize diff = button->getSize() - button->getRequestedSize(); button->setSize(button->getRequestedSize()); diff --git a/apps/openmw/mwgui/bookwindow.hpp b/apps/openmw/mwgui/bookwindow.hpp index a944f56e2..ea3057a6f 100644 --- a/apps/openmw/mwgui/bookwindow.hpp +++ b/apps/openmw/mwgui/bookwindow.hpp @@ -5,7 +5,7 @@ #include "../mwworld/ptr.hpp" -#include "imagebutton.hpp" +#include namespace MWGui { @@ -31,20 +31,24 @@ namespace MWGui void updatePages(); void clearPages(); - void adjustButton(MWGui::ImageButton* button); + void adjustButton(Gui::ImageButton* button); private: - MWGui::ImageButton* mCloseButton; - MWGui::ImageButton* mTakeButton; - MWGui::ImageButton* mNextPageButton; - MWGui::ImageButton* mPrevPageButton; + typedef std::pair Page; + typedef std::vector Pages; + + Gui::ImageButton* mCloseButton; + Gui::ImageButton* mTakeButton; + Gui::ImageButton* mNextPageButton; + Gui::ImageButton* mPrevPageButton; + MyGUI::TextBox* mLeftPageNumber; MyGUI::TextBox* mRightPageNumber; MyGUI::Widget* mLeftPage; MyGUI::Widget* mRightPage; unsigned int mCurrentPage; // 0 is first page - std::vector mPages; + Pages mPages; MWWorld::Ptr mBook; diff --git a/apps/openmw/mwgui/charactercreation.cpp b/apps/openmw/mwgui/charactercreation.cpp index 85f57a1a8..ab412f63b 100644 --- a/apps/openmw/mwgui/charactercreation.cpp +++ b/apps/openmw/mwgui/charactercreation.cpp @@ -1,18 +1,23 @@ #include "charactercreation.hpp" -#include "textinput.hpp" -#include "race.hpp" -#include "class.hpp" -#include "birth.hpp" -#include "review.hpp" -#include "inventorywindow.hpp" -#include #include "../mwbase/environment.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/world.hpp" +#include "../mwbase/windowmanager.hpp" + #include "../mwmechanics/npcstats.hpp" + #include "../mwworld/class.hpp" #include "../mwworld/fallback.hpp" +#include "../mwworld/esmstore.hpp" + +#include "textinput.hpp" +#include "race.hpp" +#include "class.hpp" +#include "birth.hpp" +#include "review.hpp" +#include "inventorywindow.hpp" namespace { @@ -27,11 +32,11 @@ namespace Step sGenerateClassSteps(int number) { number++; const MWWorld::Fallback* fallback=MWBase::Environment::get().getWorld()->getFallback(); - Step step = {fallback->getFallbackString("Question_"+boost::lexical_cast(number)+"_Question"), - {fallback->getFallbackString("Question_"+boost::lexical_cast(number)+"_AnswerOne"), - fallback->getFallbackString("Question_"+boost::lexical_cast(number)+"_AnswerTwo"), - fallback->getFallbackString("Question_"+boost::lexical_cast(number)+"_AnswerThree")}, - "vo\\misc\\chargen qa"+boost::lexical_cast(number)+".wav" + Step step = {fallback->getFallbackString("Question_"+MyGUI::utility::toString(number)+"_Question"), + {fallback->getFallbackString("Question_"+MyGUI::utility::toString(number)+"_AnswerOne"), + fallback->getFallbackString("Question_"+MyGUI::utility::toString(number)+"_AnswerTwo"), + fallback->getFallbackString("Question_"+MyGUI::utility::toString(number)+"_AnswerThree")}, + "vo\\misc\\chargen qa"+MyGUI::utility::toString(number)+".wav" }; return step; } diff --git a/apps/openmw/mwgui/charactercreation.hpp b/apps/openmw/mwgui/charactercreation.hpp index 03898093d..c2486c7f0 100644 --- a/apps/openmw/mwgui/charactercreation.hpp +++ b/apps/openmw/mwgui/charactercreation.hpp @@ -4,8 +4,9 @@ #include #include -#include "../mwbase/world.hpp" -#include "../mwbase/windowmanager.hpp" +#include + +#include "../mwmechanics/stat.hpp" namespace MWGui { diff --git a/apps/openmw/mwgui/class.cpp b/apps/openmw/mwgui/class.cpp index 9f6306830..3e8734c71 100644 --- a/apps/openmw/mwgui/class.cpp +++ b/apps/openmw/mwgui/class.cpp @@ -1,14 +1,29 @@ #include "class.hpp" +#include +#include +#include + #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwworld/esmstore.hpp" #include "tooltips.hpp" #undef min #undef max +namespace +{ + + bool sortClasses(const std::pair& left, const std::pair& right) + { + return left.second.compare(right.second) < 0; + } + +} + namespace MWGui { @@ -17,9 +32,6 @@ namespace MWGui GenerateClassResultDialog::GenerateClassResultDialog() : WindowModal("openmw_chargen_generate_class_result.layout") { - // Centre dialog - center(); - setText("ReflectT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sMessageQuestionAnswer1", "")); getWidget(mClassImage, "ClassImage"); @@ -34,6 +46,8 @@ namespace MWGui getWidget(okButton, "OKButton"); okButton->setCaptionWithReplacing("#{sMessageQuestionAnswer2}"); okButton->eventMouseButtonClick += MyGUI::newDelegate(this, &GenerateClassResultDialog::onOkClicked); + + center(); } std::string GenerateClassResultDialog::getClassId() const @@ -46,6 +60,8 @@ namespace MWGui mCurrentClassId = classId; mClassImage->setImageTexture(std::string("textures\\levelup\\") + mCurrentClassId + ".dds"); mClassName->setCaption(MWBase::Environment::get().getWorld()->getStore().get().find(mCurrentClassId)->mName); + + center(); } // widget controls @@ -128,8 +144,6 @@ namespace MWGui if (Misc::StringUtils::ciEqual(*mClassList->getItemDataAt(i), classId)) { mClassList->setIndexSelected(i); - MyGUI::Button* okButton; - getWidget(okButton, "OKButton"); break; } } @@ -164,9 +178,6 @@ namespace MWGui if (_index == MyGUI::ITEM_NONE) return; - MyGUI::Button* okButton; - getWidget(okButton, "OKButton"); - const std::string *classId = mClassList->getItemDataAt(_index); if (Misc::StringUtils::ciEqual(mCurrentClassId, *classId)) return; @@ -183,7 +194,7 @@ namespace MWGui const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - int index = 0; + std::vector > items; // class id, class name MWWorld::Store::iterator it = store.get().begin(); for (; it != store.get().end(); ++it) { @@ -191,8 +202,15 @@ namespace MWGui if (!playable) // Only display playable classes continue; - const std::string &id = it->mId; - mClassList->addItem(it->mName, id); + items.push_back(std::make_pair(it->mId, it->mName)); + } + std::sort(items.begin(), items.end(), sortClasses); + + int index = 0; + for (std::vector >::const_iterator it = items.begin(); it != items.end(); ++it) + { + const std::string &id = it->first; + mClassList->addItem(it->second, id); if (mCurrentClassId.empty()) { mCurrentClassId = id; @@ -276,7 +294,6 @@ namespace MWGui InfoBoxDialog::InfoBoxDialog() : WindowModal("openmw_infobox.layout") - , mCurrentButton(-1) { getWidget(mTextBox, "TextBox"); getWidget(mText, "Text"); @@ -305,7 +322,6 @@ namespace MWGui MyGUI::Gui::getInstance().destroyWidget(*it); } this->mButtons.clear(); - mCurrentButton = -1; // TODO: The buttons should be generated from a template in the layout file, ie. cloning an existing widget MyGUI::Button* button; @@ -335,11 +351,6 @@ namespace MWGui center(); } - int InfoBoxDialog::getChosenButton() const - { - return mCurrentButton; - } - void InfoBoxDialog::onButtonClicked(MyGUI::Widget* _sender) { std::vector::const_iterator end = mButtons.end(); @@ -348,7 +359,6 @@ namespace MWGui { if (*it == _sender) { - mCurrentButton = i; eventButtonSelected(i); return; } @@ -670,8 +680,6 @@ namespace MWGui // Centre dialog center(); - setText("LabelT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sSpecializationMenu1", "")); - getWidget(mSpecialization0, "Specialization0"); getWidget(mSpecialization1, "Specialization1"); getWidget(mSpecialization2, "Specialization2"); @@ -693,7 +701,6 @@ namespace MWGui MyGUI::Button* cancelButton; getWidget(cancelButton, "CancelButton"); - cancelButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sCancel", "")); cancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SelectSpecializationDialog::onCancelClicked); } @@ -736,8 +743,6 @@ namespace MWGui // Centre dialog center(); - setText("LabelT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sAttributesMenu1", "")); - for (int i = 0; i < 8; ++i) { Widgets::MWAttributePtr attribute; @@ -751,7 +756,6 @@ namespace MWGui MyGUI::Button* cancelButton; getWidget(cancelButton, "CancelButton"); - cancelButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sCancel", "")); cancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SelectAttributeDialog::onCancelClicked); } @@ -783,15 +787,11 @@ namespace MWGui SelectSkillDialog::SelectSkillDialog() : WindowModal("openmw_chargen_select_skill.layout") + , mSkillId(ESM::Skill::Block) { // Centre dialog center(); - setText("LabelT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sSkillsMenu1", "")); - setText("CombatLabelT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sSpecializationCombat", "")); - setText("MagicLabelT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sSpecializationMagic", "")); - setText("StealthLabelT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sSpecializationStealth", "")); - for(int i = 0; i < 9; i++) { char theIndex = '0'+i; @@ -848,7 +848,6 @@ namespace MWGui MyGUI::Button* cancelButton; getWidget(cancelButton, "CancelButton"); - cancelButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sCancel", "")); cancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SelectSkillDialog::onCancelClicked); } diff --git a/apps/openmw/mwgui/class.hpp b/apps/openmw/mwgui/class.hpp index 5c23c834d..e36a9a98b 100644 --- a/apps/openmw/mwgui/class.hpp +++ b/apps/openmw/mwgui/class.hpp @@ -1,15 +1,11 @@ #ifndef MWGUI_CLASS_H #define MWGUI_CLASS_H - +#include +#include #include "widgets.hpp" #include "windowbase.hpp" -/* - This file contains the dialogs for choosing a class. - Layout is defined by resources/mygui/openmw_chargen_class.layout. - */ - namespace MWGui { class InfoBoxDialog : public WindowModal @@ -24,7 +20,6 @@ namespace MWGui void setButtons(ButtonList &buttons); virtual void open(); - int getChosenButton() const; // Events typedef MyGUI::delegates::CMultiDelegate1 EventHandle_Int; @@ -41,7 +36,6 @@ namespace MWGui void fitToText(MyGUI::TextBox* widget); void layoutVertically(MyGUI::Widget* widget, int margin); - int mCurrentButton; MyGUI::Widget* mTextBox; MyGUI::TextBox* mText; MyGUI::Widget* mButtonBar; @@ -79,6 +73,11 @@ namespace MWGui */ EventHandle_Void eventBack; + /** Event : Dialog finished, OK button clicked.\n + signature : void method()\n + */ + EventHandle_WindowBase eventDone; + protected: void onOkClicked(MyGUI::Widget* _sender); void onBackClicked(MyGUI::Widget* _sender); @@ -109,6 +108,11 @@ namespace MWGui */ EventHandle_Void eventBack; + /** Event : Dialog finished, OK button clicked.\n + signature : void method()\n + */ + EventHandle_WindowBase eventDone; + protected: void onSelectClass(MyGUI::ListBox* _sender, size_t _index); void onAccept(MyGUI::ListBox* _sender, size_t _index); @@ -238,6 +242,11 @@ namespace MWGui std::string getTextInput() const { return mTextEdit->getCaption(); } void setTextInput(const std::string &text) { mTextEdit->setCaption(text); } + /** Event : Dialog finished, OK button clicked.\n + signature : void method()\n + */ + EventHandle_WindowBase eventDone; + protected: void onOkClicked(MyGUI::Widget* _sender); @@ -268,6 +277,11 @@ namespace MWGui */ EventHandle_Void eventBack; + /** Event : Dialog finished, OK button clicked.\n + signature : void method()\n + */ + EventHandle_WindowBase eventDone; + protected: void onOkClicked(MyGUI::Widget* _sender); void onBackClicked(MyGUI::Widget* _sender); diff --git a/apps/openmw/mwgui/companionitemmodel.cpp b/apps/openmw/mwgui/companionitemmodel.cpp index b8be9dcb8..983ef5017 100644 --- a/apps/openmw/mwgui/companionitemmodel.cpp +++ b/apps/openmw/mwgui/companionitemmodel.cpp @@ -3,6 +3,22 @@ #include "../mwmechanics/npcstats.hpp" #include "../mwworld/class.hpp" +namespace +{ + + void modifyProfit(const MWWorld::Ptr& actor, int diff) + { + std::string script = actor.getClass().getScript(actor); + if (!script.empty()) + { + int profit = actor.getRefData().getLocals().getIntVar(script, "minimumprofit"); + profit += diff; + actor.getRefData().getLocals().setVarByInt(script, "minimumprofit", profit); + } + } + +} + namespace MWGui { CompanionItemModel::CompanionItemModel(const MWWorld::Ptr &actor) @@ -12,23 +28,25 @@ namespace MWGui MWWorld::Ptr CompanionItemModel::copyItem (const ItemStack& item, size_t count, bool setNewOwner=false) { - if (mActor.getClass().isNpc()) - { - MWMechanics::NpcStats& stats = mActor.getClass().getNpcStats(mActor); - stats.modifyProfit(item.mBase.getClass().getValue(item.mBase) * count); - } + if (hasProfit(mActor)) + modifyProfit(mActor, item.mBase.getClass().getValue(item.mBase) * count); return InventoryItemModel::copyItem(item, count, setNewOwner); } void CompanionItemModel::removeItem (const ItemStack& item, size_t count) { - if (mActor.getClass().isNpc()) - { - MWMechanics::NpcStats& stats = mActor.getClass().getNpcStats(mActor); - stats.modifyProfit(-item.mBase.getClass().getValue(item.mBase) * count); - } + if (hasProfit(mActor)) + modifyProfit(mActor, -item.mBase.getClass().getValue(item.mBase) * count); InventoryItemModel::removeItem(item, count); } + + bool CompanionItemModel::hasProfit(const MWWorld::Ptr &actor) + { + std::string script = actor.getClass().getScript(actor); + if (script.empty()) + return false; + return actor.getRefData().getLocals().hasVar(script, "minimumprofit"); + } } diff --git a/apps/openmw/mwgui/companionitemmodel.hpp b/apps/openmw/mwgui/companionitemmodel.hpp index 172fa9508..4c77ee12f 100644 --- a/apps/openmw/mwgui/companionitemmodel.hpp +++ b/apps/openmw/mwgui/companionitemmodel.hpp @@ -15,6 +15,8 @@ namespace MWGui virtual MWWorld::Ptr copyItem (const ItemStack& item, size_t count, bool setNewOwner); virtual void removeItem (const ItemStack& item, size_t count); + + bool hasProfit(const MWWorld::Ptr& actor); }; } diff --git a/apps/openmw/mwgui/companionwindow.cpp b/apps/openmw/mwgui/companionwindow.cpp index 6ac8c9d18..8f709ec8d 100644 --- a/apps/openmw/mwgui/companionwindow.cpp +++ b/apps/openmw/mwgui/companionwindow.cpp @@ -1,9 +1,10 @@ #include "companionwindow.hpp" -#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/dialoguemanager.hpp" +#include "../mwbase/windowmanager.hpp" #include "../mwmechanics/npcstats.hpp" @@ -13,9 +14,24 @@ #include "itemview.hpp" #include "sortfilteritemmodel.hpp" #include "companionitemmodel.hpp" -#include "container.hpp" +#include "draganddrop.hpp" #include "countdialog.hpp" +namespace +{ + + int getProfit(const MWWorld::Ptr& actor) + { + std::string script = actor.getClass().getScript(actor); + if (!script.empty()) + { + return actor.getRefData().getLocals().getIntVar(script, "minimumprofit"); + } + return 0; + } + +} + namespace MWGui { @@ -50,6 +66,13 @@ void CompanionWindow::onItemSelected(int index) const ItemStack& item = mSortModel->getItem(index); + // We can't take conjured items from a companion NPC + if (item.mFlags & ItemStack::Flag_Bound) + { + MWBase::Environment::get().getWindowManager()->messageBox("#{sBarterDialog12}"); + return; + } + MWWorld::Ptr object = item.mBase; int count = item.mCount; bool shift = MyGUI::InputManager::getInstance().isShiftPressed(); @@ -108,13 +131,12 @@ void CompanionWindow::updateEncumbranceBar() float encumbrance = mPtr.getClass().getEncumbrance(mPtr); mEncumbranceBar->setValue(encumbrance, capacity); - if (mPtr.getTypeName() != typeid(ESM::NPC).name()) - mProfitLabel->setCaption(""); - else + if (mModel && mModel->hasProfit(mPtr)) { - MWMechanics::NpcStats& stats = mPtr.getClass().getNpcStats(mPtr); - mProfitLabel->setCaptionWithReplacing("#{sProfitValue} " + boost::lexical_cast(stats.getProfit())); + mProfitLabel->setCaptionWithReplacing("#{sProfitValue} " + MyGUI::utility::toString(getProfit(mPtr))); } + else + mProfitLabel->setCaption(""); } void CompanionWindow::onCloseButtonClicked(MyGUI::Widget* _sender) @@ -124,7 +146,7 @@ void CompanionWindow::onCloseButtonClicked(MyGUI::Widget* _sender) void CompanionWindow::exit() { - if (mPtr.getTypeName() == typeid(ESM::NPC).name() && mPtr.getClass().getNpcStats(mPtr).getProfit() < 0) + if (mModel && mModel->hasProfit(mPtr) && getProfit(mPtr) < 0) { std::vector buttons; buttons.push_back("#{sCompanionWarningButtonOne}"); @@ -140,11 +162,9 @@ void CompanionWindow::onMessageBoxButtonClicked(int button) { if (button == 0) { - mPtr.getRefData().getLocals().setVarByInt(mPtr.getClass().getScript(mPtr), - "minimumProfit", mPtr.getClass().getNpcStats(mPtr).getProfit()); - MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Companion); - MWBase::Environment::get().getDialogueManager()->startDialogue (mPtr); + // Important for Calvus' contract script to work properly + MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Dialogue); } } diff --git a/apps/openmw/mwgui/confirmationdialog.cpp b/apps/openmw/mwgui/confirmationdialog.cpp index 57c88bfa2..83650b195 100644 --- a/apps/openmw/mwgui/confirmationdialog.cpp +++ b/apps/openmw/mwgui/confirmationdialog.cpp @@ -1,5 +1,8 @@ #include "confirmationdialog.hpp" +#include +#include + namespace MWGui { ConfirmationDialog::ConfirmationDialog() : @@ -13,12 +16,15 @@ namespace MWGui mOkButton->eventMouseButtonClick += MyGUI::newDelegate(this, &ConfirmationDialog::onOkButtonClicked); } - void ConfirmationDialog::open(const std::string& message) + void ConfirmationDialog::open(const std::string& message, const std::string& confirmMessage, const std::string& cancelMessage) { setVisible(true); mMessage->setCaptionWithReplacing(message); + mCancelButton->setCaptionWithReplacing(cancelMessage); + mOkButton->setCaptionWithReplacing(confirmMessage); + int height = mMessage->getTextSize().height + 72; mMainWidget->setSize(mMainWidget->getWidth(), height); diff --git a/apps/openmw/mwgui/confirmationdialog.hpp b/apps/openmw/mwgui/confirmationdialog.hpp index c50873c74..7ff16b10a 100644 --- a/apps/openmw/mwgui/confirmationdialog.hpp +++ b/apps/openmw/mwgui/confirmationdialog.hpp @@ -9,7 +9,7 @@ namespace MWGui { public: ConfirmationDialog(); - void open(const std::string& message); + void open(const std::string& message, const std::string& confirmMessage="#{sOk}", const std::string& cancelMessage="#{sCancel}"); virtual void exit(); typedef MyGUI::delegates::CMultiDelegate0 EventHandle_Void; diff --git a/apps/openmw/mwgui/console.cpp b/apps/openmw/mwgui/console.cpp index 9f67524ae..4eb9a271c 100644 --- a/apps/openmw/mwgui/console.cpp +++ b/apps/openmw/mwgui/console.cpp @@ -1,5 +1,7 @@ #include "console.hpp" +#include + #include #include @@ -10,6 +12,7 @@ #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" #include "../mwworld/esmstore.hpp" @@ -101,8 +104,20 @@ namespace MWGui it->second->listIdentifier (mNames); } + // exterior cell names aren't technically identifiers, but since the COC function accepts them, + // we should list them too + for (MWWorld::Store::iterator it = store.get().extBegin(); + it != store.get().extEnd(); ++it) + { + if (!it->mName.empty()) + mNames.push_back(it->mName); + } + // sort std::sort (mNames.begin(), mNames.end()); + + // remove duplicates + mNames.erase( std::unique( mNames.begin(), mNames.end() ), mNames.end() ); } } @@ -140,6 +155,7 @@ namespace MWGui void Console::close() { // Apparently, hidden widgets can retain key focus + // Remove for MyGUI 3.2.2 MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(NULL); } @@ -154,11 +170,6 @@ namespace MWGui mCommandLine->setFontName(fntName); } - void Console::clearHistory() - { - mHistory->setCaption(""); - } - void Console::print(const std::string &msg) { mHistory->addText(msg); @@ -279,7 +290,8 @@ namespace MWGui // Add the command to the history, and set the current pointer to // the end of the list - mCommandHistory.push_back(cm); + if (mCommandHistory.empty() || mCommandHistory.back() != cm) + mCommandHistory.push_back(cm); mCurrent = mCommandHistory.end(); mEditString.clear(); diff --git a/apps/openmw/mwgui/console.hpp b/apps/openmw/mwgui/console.hpp index 90da740c2..09a05be48 100644 --- a/apps/openmw/mwgui/console.hpp +++ b/apps/openmw/mwgui/console.hpp @@ -48,8 +48,6 @@ namespace MWGui void onResChange(int width, int height); - void clearHistory(); - // Print a message to the console. Messages may contain color // code, eg. "#FFFFFF this is white". void print(const std::string &msg); diff --git a/apps/openmw/mwgui/container.cpp b/apps/openmw/mwgui/container.cpp index f0212031e..579730f42 100644 --- a/apps/openmw/mwgui/container.cpp +++ b/apps/openmw/mwgui/container.cpp @@ -1,6 +1,7 @@ #include "container.hpp" -#include +#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -24,111 +25,11 @@ #include "inventoryitemmodel.hpp" #include "sortfilteritemmodel.hpp" #include "pickpocketitemmodel.hpp" +#include "draganddrop.hpp" namespace MWGui { - void DragAndDrop::startDrag (int index, SortFilterItemModel* sortModel, ItemModel* sourceModel, ItemView* sourceView, int count) - { - mItem = sourceModel->getItem(index); - mDraggedCount = count; - mSourceModel = sourceModel; - mSourceView = sourceView; - mSourceSortModel = sortModel; - mIsOnDragAndDrop = true; - mDragAndDropWidget->setVisible(true); - - // If picking up an item that isn't from the player's inventory, the item gets added to player inventory backend - // immediately, even though it's still floating beneath the mouse cursor. A bit counterintuitive, - // but this is how it works in vanilla, and not doing so would break quests (BM_beasts for instance). - ItemModel* playerModel = MWBase::Environment::get().getWindowManager()->getInventoryWindow()->getModel(); - if (mSourceModel != playerModel) - { - MWWorld::Ptr item = mSourceModel->moveItem(mItem, mDraggedCount, playerModel); - - playerModel->update(); - - ItemModel::ModelIndex newIndex = -1; - for (unsigned int i=0; igetItemCount(); ++i) - { - if (playerModel->getItem(i).mBase == item) - { - newIndex = i; - break; - } - } - mItem = playerModel->getItem(newIndex); - mSourceModel = playerModel; - - SortFilterItemModel* playerFilterModel = MWBase::Environment::get().getWindowManager()->getInventoryWindow()->getSortFilterModel(); - mSourceSortModel = playerFilterModel; - } - - std::string sound = mItem.mBase.getClass().getUpSoundId(mItem.mBase); - MWBase::Environment::get().getSoundManager()->playSound (sound, 1.0, 1.0); - - if (mSourceSortModel) - { - mSourceSortModel->clearDragItems(); - mSourceSortModel->addDragItem(mItem.mBase, count); - } - - ItemWidget* baseWidget = mDragAndDropWidget->createWidget - ("MW_ItemIcon", MyGUI::IntCoord(0, 0, 42, 42), MyGUI::Align::Default); - mDraggedWidget = baseWidget; - baseWidget->setItem(mItem.mBase); - baseWidget->setNeedMouseFocus(false); - - // text widget that shows item count - // TODO: move to ItemWidget - MyGUI::TextBox* text = baseWidget->createWidget("SandBrightText", - MyGUI::IntCoord(0, 14, 32, 18), MyGUI::Align::Default, std::string("Label")); - text->setTextAlign(MyGUI::Align::Right); - text->setNeedMouseFocus(false); - text->setTextShadow(true); - text->setTextShadowColour(MyGUI::Colour(0,0,0)); - text->setCaption(ItemView::getCountString(count)); - - sourceView->update(); - - MWBase::Environment::get().getWindowManager()->setDragDrop(true); - } - - void DragAndDrop::drop(ItemModel *targetModel, ItemView *targetView) - { - std::string sound = mItem.mBase.getClass().getDownSoundId(mItem.mBase); - MWBase::Environment::get().getSoundManager()->playSound (sound, 1.0, 1.0); - - mDragAndDropWidget->setVisible(false); - - // If item is dropped where it was taken from, we don't need to do anything - - // otherwise, do the transfer - if (targetModel != mSourceModel) - { - mSourceModel->moveItem(mItem, mDraggedCount, targetModel); - } - - mSourceModel->update(); - - finish(); - if (targetView) - targetView->update(); - - // We need to update the view since an other item could be auto-equipped. - mSourceView->update(); - } - - void DragAndDrop::finish() - { - mIsOnDragAndDrop = false; - mSourceSortModel->clearDragItems(); - - MyGUI::Gui::getInstance().destroyWidget(mDraggedWidget); - mDraggedWidget = 0; - MWBase::Environment::get().getWindowManager()->setDragDrop(false); - } - - ContainerWindow::ContainerWindow(DragAndDrop* dragAndDrop) : WindowBase("openmw_container_window.layout") , mDragAndDrop(dragAndDrop) @@ -164,6 +65,13 @@ namespace MWGui const ItemStack& item = mSortModel->getItem(index); + // We can't take a conjured item from a container (some NPC we're pickpocketing, a box, etc) + if (item.mFlags & ItemStack::Flag_Bound) + { + MWBase::Environment::get().getWindowManager()->messageBox("#{sContentsMessage1}"); + return; + } + MWWorld::Ptr object = item.mBase; int count = item.mCount; bool shift = MyGUI::InputManager::getInstance().isShiftPressed(); @@ -232,7 +140,8 @@ namespace MWGui { // we are stealing stuff MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); - mModel = new PickpocketItemModel(player, new InventoryItemModel(container)); + mModel = new PickpocketItemModel(player, new InventoryItemModel(container), + !mPtr.getClass().getCreatureStats(mPtr).getKnockedDown()); } else mModel = new InventoryItemModel(container); @@ -245,8 +154,6 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mCloseButton); - // Careful here. setTitle may cause size updates, causing itemview redraw, so make sure to do it last - // or we end up using a possibly invalid model. setTitle(container.getClass().getName(container)); } @@ -281,10 +188,9 @@ namespace MWGui MWMechanics::Pickpocket pickpocket(player, mPtr); if (pickpocket.finish()) { - MWBase::Environment::get().getMechanicsManager()->reportCrime( - player, mPtr, MWBase::MechanicsManager::OT_Pickpocket); + MWBase::Environment::get().getMechanicsManager()->commitCrime( + player, mPtr, MWBase::MechanicsManager::OT_Pickpocket, 0, true); MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_Container); - MWBase::Environment::get().getDialogueManager()->say(mPtr, "Thief"); mPickpocketDetected = true; return; } @@ -356,16 +262,17 @@ namespace MWGui bool ContainerWindow::onTakeItem(const ItemStack &item, int count) { MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); - if (dynamic_cast(mModel)) + // TODO: move to ItemModels + if (dynamic_cast(mModel) + && !mPtr.getClass().getCreatureStats(mPtr).getKnockedDown()) { MWMechanics::Pickpocket pickpocket(player, mPtr); if (pickpocket.pick(item.mBase, count)) { int value = item.mBase.getClass().getValue(item.mBase) * count; - MWBase::Environment::get().getMechanicsManager()->reportCrime( - player, MWWorld::Ptr(), MWBase::MechanicsManager::OT_Theft, value); + MWBase::Environment::get().getMechanicsManager()->commitCrime( + player, MWWorld::Ptr(), MWBase::MechanicsManager::OT_Theft, value, true); MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_Container); - MWBase::Environment::get().getDialogueManager()->say(mPtr, "Thief"); mPickpocketDetected = true; return false; } @@ -378,7 +285,7 @@ namespace MWGui if (mPtr.getClass().isActor() && mPtr.getClass().getCreatureStats(mPtr).isDead()) return true; else - MWBase::Environment::get().getMechanicsManager()->itemTaken(player, item.mBase, count); + MWBase::Environment::get().getMechanicsManager()->itemTaken(player, item.mBase, mPtr, count); } return true; } diff --git a/apps/openmw/mwgui/container.hpp b/apps/openmw/mwgui/container.hpp index 79951f70e..87ae993a5 100644 --- a/apps/openmw/mwgui/container.hpp +++ b/apps/openmw/mwgui/container.hpp @@ -28,24 +28,6 @@ namespace MWGui namespace MWGui { - class DragAndDrop - { - public: - bool mIsOnDragAndDrop; - MyGUI::Widget* mDraggedWidget; - MyGUI::Widget* mDragAndDropWidget; - ItemModel* mSourceModel; - ItemView* mSourceView; - SortFilterItemModel* mSourceSortModel; - ItemStack mItem; - int mDraggedCount; - - void startDrag (int index, SortFilterItemModel* sortModel, ItemModel* sourceModel, ItemView* sourceView, int count); - void drop (ItemModel* targetModel, ItemView* targetView); - - void finish(); - }; - class ContainerWindow : public WindowBase, public ReferenceInterface { public: diff --git a/apps/openmw/mwgui/controllers.cpp b/apps/openmw/mwgui/controllers.cpp index e62fb3fce..ad804997a 100644 --- a/apps/openmw/mwgui/controllers.cpp +++ b/apps/openmw/mwgui/controllers.cpp @@ -1,11 +1,14 @@ #include "controllers.hpp" +#include +#include + namespace MWGui { namespace Controllers { - ControllerRepeatClick::ControllerRepeatClick() : + ControllerRepeatEvent::ControllerRepeatEvent() : mInit(0.5), mStep(0.1), mEnabled(true), @@ -13,11 +16,11 @@ namespace MWGui { } - ControllerRepeatClick::~ControllerRepeatClick() + ControllerRepeatEvent::~ControllerRepeatEvent() { } - bool ControllerRepeatClick::addTime(MyGUI::Widget* _widget, float _time) + bool ControllerRepeatEvent::addTime(MyGUI::Widget* _widget, float _time) { if(mTimeLeft == 0) mTimeLeft = mInit; @@ -31,24 +34,36 @@ namespace MWGui return true; } - void ControllerRepeatClick::setRepeat(float init, float step) + void ControllerRepeatEvent::setRepeat(float init, float step) { mInit = init; mStep = step; } - void ControllerRepeatClick::setEnabled(bool enable) + void ControllerRepeatEvent::setEnabled(bool enable) { mEnabled = enable; } - void ControllerRepeatClick::setProperty(const std::string& _key, const std::string& _value) + void ControllerRepeatEvent::setProperty(const std::string& _key, const std::string& _value) { } - void ControllerRepeatClick::prepareItem(MyGUI::Widget* _widget) + void ControllerRepeatEvent::prepareItem(MyGUI::Widget* _widget) + { + } + + // ------------------------------------------------------------- + + void ControllerFollowMouse::prepareItem(MyGUI::Widget *_widget) { } + bool ControllerFollowMouse::addTime(MyGUI::Widget *_widget, float _time) + { + _widget->setPosition(MyGUI::InputManager::getInstance().getMousePosition()); + return true; + } + } } diff --git a/apps/openmw/mwgui/controllers.hpp b/apps/openmw/mwgui/controllers.hpp index 798acde62..4208f048c 100644 --- a/apps/openmw/mwgui/controllers.hpp +++ b/apps/openmw/mwgui/controllers.hpp @@ -1,22 +1,27 @@ #ifndef MWGUI_CONTROLLERS_H #define MWGUI_CONTROLLERS_H -#include +#include #include +namespace MyGUI +{ + class Widget; +} namespace MWGui { namespace Controllers { - class ControllerRepeatClick : + // Should be removed when upgrading to MyGUI 3.2.2 (current git), it has ControllerRepeatClick + class ControllerRepeatEvent : public MyGUI::ControllerItem { - MYGUI_RTTI_DERIVED( ControllerRepeatClick ) + MYGUI_RTTI_DERIVED( ControllerRepeatEvent ) public: - ControllerRepeatClick(); - virtual ~ControllerRepeatClick(); + ControllerRepeatEvent(); + virtual ~ControllerRepeatEvent(); void setRepeat(float init, float step); void setEnabled(bool enable); @@ -40,6 +45,17 @@ namespace MWGui bool mEnabled; float mTimeLeft; }; + + /// Automatically positions a widget below the mouse cursor. + class ControllerFollowMouse : + public MyGUI::ControllerItem + { + MYGUI_RTTI_DERIVED( ControllerFollowMouse ) + + private: + bool addTime(MyGUI::Widget* _widget, float _time); + void prepareItem(MyGUI::Widget* _widget); + }; } } diff --git a/apps/openmw/mwgui/countdialog.cpp b/apps/openmw/mwgui/countdialog.cpp index 53c33b3c4..c6f2180c9 100644 --- a/apps/openmw/mwgui/countdialog.cpp +++ b/apps/openmw/mwgui/countdialog.cpp @@ -1,6 +1,10 @@ #include "countdialog.hpp" -#include +#include +#include +#include + +#include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" @@ -19,7 +23,7 @@ namespace MWGui mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &CountDialog::onCancelButtonClicked); mOkButton->eventMouseButtonClick += MyGUI::newDelegate(this, &CountDialog::onOkButtonClicked); - mItemEdit->eventEditTextChange += MyGUI::newDelegate(this, &CountDialog::onEditTextChange); + mItemEdit->eventValueChanged += MyGUI::newDelegate(this, &CountDialog::onEditValueChanged); mSlider->eventScrollChangePosition += MyGUI::newDelegate(this, &CountDialog::onSliderMoved); // make sure we read the enter key being pressed to accept multiple items mItemEdit->eventEditSelectAccept += MyGUI::newDelegate(this, &CountDialog::onEnterKeyPressed); @@ -46,7 +50,10 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mItemEdit); mSlider->setScrollPosition(maxCount-1); - mItemEdit->setCaption(boost::lexical_cast(maxCount)); + + mItemEdit->setMinValue(1); + mItemEdit->setMaxValue(maxCount); + mItemEdit->setValue(maxCount); } void CountDialog::cancel() //Keeping this here as I don't know if anything else relies on it. @@ -80,30 +87,13 @@ namespace MWGui setVisible(false); } - void CountDialog::onEditTextChange(MyGUI::EditBox* _sender) + void CountDialog::onEditValueChanged(int value) { - if (_sender->getCaption() == "") - return; - - unsigned int count; - try - { - count = boost::lexical_cast(_sender->getCaption()); - } - catch (std::bad_cast&) - { - count = 1; - } - if (count > mSlider->getScrollRange()) - { - count = mSlider->getScrollRange(); - } - mSlider->setScrollPosition(count-1); - onSliderMoved(mSlider, count-1); + mSlider->setScrollPosition(value-1); } void CountDialog::onSliderMoved(MyGUI::ScrollBar* _sender, size_t _position) { - mItemEdit->setCaption(boost::lexical_cast(_position+1)); + mItemEdit->setValue(_position+1); } } diff --git a/apps/openmw/mwgui/countdialog.hpp b/apps/openmw/mwgui/countdialog.hpp index a00b0b223..a54e99cf4 100644 --- a/apps/openmw/mwgui/countdialog.hpp +++ b/apps/openmw/mwgui/countdialog.hpp @@ -3,6 +3,11 @@ #include "windowbase.hpp" +namespace Gui +{ + class NumericEditBox; +} + namespace MWGui { class CountDialog : public WindowModal @@ -22,7 +27,7 @@ namespace MWGui private: MyGUI::ScrollBar* mSlider; - MyGUI::EditBox* mItemEdit; + Gui::NumericEditBox* mItemEdit; MyGUI::TextBox* mItemText; MyGUI::TextBox* mLabelText; MyGUI::Button* mOkButton; @@ -30,7 +35,7 @@ namespace MWGui void onCancelButtonClicked(MyGUI::Widget* _sender); void onOkButtonClicked(MyGUI::Widget* _sender); - void onEditTextChange(MyGUI::EditBox* _sender); + void onEditValueChanged(int value); void onSliderMoved(MyGUI::ScrollBar* _sender, size_t _position); void onEnterKeyPressed(MyGUI::EditBox* _sender); }; diff --git a/apps/openmw/mwgui/debugwindow.cpp b/apps/openmw/mwgui/debugwindow.cpp new file mode 100644 index 000000000..a6dab66c1 --- /dev/null +++ b/apps/openmw/mwgui/debugwindow.cpp @@ -0,0 +1,120 @@ +#include "debugwindow.hpp" + +#include +#include +#include +#include + +#include + +namespace +{ + void bulletDumpRecursive(CProfileIterator* pit, int spacing, std::stringstream& os) + { + pit->First(); + if (pit->Is_Done()) + return; + + float accumulated_time=0,parent_time = pit->Is_Root() ? CProfileManager::Get_Time_Since_Reset() : pit->Get_Current_Parent_Total_Time(); + int i,j; + int frames_since_reset = CProfileManager::Get_Frame_Count_Since_Reset(); + for (i=0;iGet_Current_Parent_Name())+" (total running time: "+MyGUI::utility::toString(parent_time,3)+" ms) ---\n"; + os << s; + //float totalTime = 0.f; + + int numChildren = 0; + + for (i = 0; !pit->Is_Done(); i++,pit->Next()) + { + numChildren++; + float current_total_time = pit->Get_Current_Total_Time(); + accumulated_time += current_total_time; + float fraction = parent_time > SIMD_EPSILON ? (current_total_time / parent_time) * 100 : 0.f; + + for (j=0;jGet_Current_Name()+" ("+MyGUI::utility::toString(fraction,2)+" %) :: "+MyGUI::utility::toString(ms,3)+" ms / frame ("+MyGUI::utility::toString(pit->Get_Current_Total_Calls())+" calls)\n"; + os << s; + //totalTime += current_total_time; + //recurse into children + } + + if (parent_time < accumulated_time) + { + os << "what's wrong\n"; + } + for (i=0;i SIMD_EPSILON ? ((parent_time - accumulated_time) / parent_time) * 100 : 0.f; + s = "Unaccounted: ("+MyGUI::utility::toString(unaccounted,3)+" %) :: "+MyGUI::utility::toString(parent_time - accumulated_time,3)+" ms\n"; + os << s; + + for (i=0;iEnter_Child(i); + bulletDumpRecursive(pit, spacing+3, os); + pit->Enter_Parent(); + } + } + + void bulletDumpAll(std::stringstream& os) + { + CProfileIterator* profileIterator = 0; + profileIterator = CProfileManager::Get_Iterator(); + + bulletDumpRecursive(profileIterator, 0, os); + + CProfileManager::Release_Iterator(profileIterator); + } +} + + +namespace MWGui +{ + + DebugWindow::DebugWindow() + : WindowBase("openmw_debug_window.layout") + { + getWidget(mTabControl, "TabControl"); + + // Ideas for other tabs: + // - Texture / compositor texture viewer + // - Log viewer + // - Material editor + // - Shader editor + + MyGUI::TabItem* item = mTabControl->addItem("Physics Profiler"); + mBulletProfilerEdit = item->createWidgetReal + ("LogEdit", MyGUI::FloatCoord(0,0,1,1), MyGUI::Align::Stretch); + + MyGUI::IntSize viewSize = MyGUI::RenderManager::getInstance().getViewSize(); + mMainWidget->setSize(viewSize); + } + + void DebugWindow::onFrame(float dt) + { + if (!isVisible()) + return; + + static float timer = 0; + timer -= dt; + + if (timer > 0) + return; + timer = 1; + + std::stringstream stream; + bulletDumpAll(stream); + + if (mBulletProfilerEdit->isTextSelection()) // pause updating while user is trying to copy text + return; + + size_t previousPos = mBulletProfilerEdit->getVScrollPosition(); + mBulletProfilerEdit->setCaption(stream.str()); + mBulletProfilerEdit->setVScrollPosition(std::min(previousPos, mBulletProfilerEdit->getVScrollRange()-1)); + } + +} diff --git a/apps/openmw/mwgui/debugwindow.hpp b/apps/openmw/mwgui/debugwindow.hpp new file mode 100644 index 000000000..af5b914ea --- /dev/null +++ b/apps/openmw/mwgui/debugwindow.hpp @@ -0,0 +1,24 @@ +#ifndef OPENMW_MWGUI_DEBUGWINDOW_H +#define OPENMW_MWGUI_DEBUGWINDOW_H + +#include "windowbase.hpp" + +namespace MWGui +{ + + class DebugWindow : public WindowBase + { + public: + DebugWindow(); + + void onFrame(float dt); + + private: + MyGUI::TabControl* mTabControl; + + MyGUI::EditBox* mBulletProfilerEdit; + }; + +} + +#endif diff --git a/apps/openmw/mwgui/dialogue.cpp b/apps/openmw/mwgui/dialogue.cpp index 2283e0cbe..eee86c6d2 100644 --- a/apps/openmw/mwgui/dialogue.cpp +++ b/apps/openmw/mwgui/dialogue.cpp @@ -1,7 +1,12 @@ #include "dialogue.hpp" #include -#include + +#include +#include +#include + +#include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" @@ -13,11 +18,11 @@ #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwdialogue/dialoguemanagerimp.hpp" #include "widgets.hpp" -#include "list.hpp" #include "tradewindow.hpp" #include "spellbuyingwindow.hpp" #include "travelwindow.hpp" @@ -25,6 +30,16 @@ #include "journalbooks.hpp" // to_utf8_span +namespace +{ + + MyGUI::Colour getTextColour (const std::string& type) + { + return MyGUI::Colour::parse(MyGUI::LanguageManager::getInstance().replaceTags("#{fontcolour=" + type + "}")); + } + +} + namespace MWGui { @@ -84,7 +99,7 @@ namespace MWGui mBribe100Button->setEnabled (playerGold >= 100); mBribe1000Button->setEnabled (playerGold >= 1000); - mGoldLabel->setCaptionWithReplacing("#{sGold}: " + boost::lexical_cast(playerGold)); + mGoldLabel->setCaptionWithReplacing("#{sGold}: " + MyGUI::utility::toString(playerGold)); } void PersuasionDialog::exit() @@ -102,7 +117,7 @@ namespace MWGui void Response::write(BookTypesetter::Ptr typesetter, KeywordSearchT* keywordSearch, std::map& topicLinks) const { - BookTypesetter::Style* title = typesetter->createStyle("", MyGUI::Colour(223/255.f, 201/255.f, 159/255.f)); + BookTypesetter::Style* title = typesetter->createStyle("", getTextColour("header")); typesetter->sectionBreak(9); if (mTitle != "") typesetter->write(title, to_utf8_span(mTitle.c_str())); @@ -114,10 +129,10 @@ namespace MWGui // We need this copy for when @# hyperlinks are replaced std::string text = mText; - size_t pos_begin, pos_end; + size_t pos_end; for(;;) { - pos_begin = text.find('@'); + size_t pos_begin = text.find('@'); if (pos_begin != std::string::npos) pos_end = text.find('#', pos_begin); @@ -146,14 +161,14 @@ namespace MWGui if (hyperLinks.size() && MWBase::Environment::get().getWindowManager()->getTranslationDataStorage().hasTranslation()) { - BookTypesetter::Style* style = typesetter->createStyle("", MyGUI::Colour(202/255.f, 165/255.f, 96/255.f)); + BookTypesetter::Style* style = typesetter->createStyle("", getTextColour("normal")); size_t formatted = 0; // points to the first character that is not laid out yet for (std::map::iterator it = hyperLinks.begin(); it != hyperLinks.end(); ++it) { intptr_t topicId = it->second; - const MyGUI::Colour linkHot (143/255.f, 155/255.f, 218/255.f); - const MyGUI::Colour linkNormal (112/255.f, 126/255.f, 207/255.f); - const MyGUI::Colour linkActive (175/255.f, 184/255.f, 228/255.f); + const MyGUI::Colour linkHot (getTextColour("link_over")); + const MyGUI::Colour linkNormal (getTextColour("link")); + const MyGUI::Colour linkActive (getTextColour("link_pressed")); BookTypesetter::Style* hotStyle = typesetter->createHotStyle (style, linkNormal, linkHot, linkActive, topicId); if (formatted < it->first.first) typesetter->write(style, formatted, it->first.first); @@ -165,11 +180,13 @@ namespace MWGui } else { - std::string::const_iterator i = text.begin (); - KeywordSearchT::Match match; + std::vector matches; + keywordSearch->highlightKeywords(text.begin(), text.end(), matches); - while (i != text.end () && keywordSearch->search (i, text.end (), match, text.begin ())) + std::string::const_iterator i = text.begin (); + for (std::vector::iterator it = matches.begin(); it != matches.end(); ++it) { + KeywordSearchT::Match match = *it; if (i != match.mBeg) addTopicLink (typesetter, 0, i - text.begin (), match.mBeg - text.begin ()); @@ -177,7 +194,6 @@ namespace MWGui i = match.mEnd; } - if (i != text.end ()) addTopicLink (typesetter, 0, i - text.begin (), text.size ()); } @@ -185,11 +201,11 @@ namespace MWGui void Response::addTopicLink(BookTypesetter::Ptr typesetter, intptr_t topicId, size_t begin, size_t end) const { - BookTypesetter::Style* style = typesetter->createStyle("", MyGUI::Colour(202/255.f, 165/255.f, 96/255.f)); + BookTypesetter::Style* style = typesetter->createStyle("", getTextColour("normal")); - const MyGUI::Colour linkHot (143/255.f, 155/255.f, 218/255.f); - const MyGUI::Colour linkNormal (112/255.f, 126/255.f, 207/255.f); - const MyGUI::Colour linkActive (175/255.f, 184/255.f, 228/255.f); + const MyGUI::Colour linkHot (getTextColour("link_over")); + const MyGUI::Colour linkNormal (getTextColour("link")); + const MyGUI::Colour linkActive (getTextColour("link_pressed")); if (topicId) style = typesetter->createHotStyle (style, linkNormal, linkHot, linkActive, topicId); @@ -203,7 +219,7 @@ namespace MWGui void Message::write(BookTypesetter::Ptr typesetter, KeywordSearchT* keywordSearch, std::map& topicLinks) const { - BookTypesetter::Style* title = typesetter->createStyle("", MyGUI::Colour(223/255.f, 201/255.f, 159/255.f)); + BookTypesetter::Style* title = typesetter->createStyle("", getTextColour("notify")); typesetter->sectionBreak(9); typesetter->write(title, to_utf8_span(mText.c_str())); } @@ -266,7 +282,7 @@ namespace MWGui BookPage::ClickCallback callback = boost::bind (&DialogueWindow::notifyLinkClicked, this, _1); mHistory->adviseLinkClicked(callback); - static_cast(mMainWidget)->eventWindowChangeCoord += MyGUI::newDelegate(this, &DialogueWindow::onWindowResize); + mMainWidget->castType()->eventWindowChangeCoord += MyGUI::newDelegate(this, &DialogueWindow::onWindowResize); } void DialogueWindow::exit() @@ -276,7 +292,6 @@ namespace MWGui { // in choice, not allowed to escape, but give access to main menu to allow loading other saves MWBase::Environment::get().getWindowManager()->pushGuiMode (MWGui::GM_MainMenu); - MWBase::Environment::get().getSoundManager()->pauseSounds (MWBase::SoundManager::Play_TypeSfx); } else MWBase::Environment::get().getDialogueManager()->goodbyeSelected(); @@ -396,6 +411,22 @@ namespace MWGui mLinks.clear(); updateOptions(); + + restock(); + } + + void DialogueWindow::restock() + { + MWMechanics::CreatureStats &sellerStats = mPtr.getClass().getCreatureStats(mPtr); + float delay = MWBase::Environment::get().getWorld()->getStore().get().find("fBarterGoldResetDelay")->getFloat(); + + // Gold is restocked every 24h + if (MWBase::Environment::get().getWorld()->getTimeStamp() >= sellerStats.getLastRestockTime() + delay) + { + sellerStats.setGoldPool(mPtr.getClass().getBaseGold(mPtr)); + + sellerStats.setLastRestockTime(MWBase::Environment::get().getWorld()->getTimeStamp()); + } } void DialogueWindow::setKeywords(std::list keyWords) @@ -472,7 +503,7 @@ namespace MWGui mScrollBar->setVisible(true); } - BookTypesetter::Ptr typesetter = BookTypesetter::create (mHistory->getWidth(), std::numeric_limits().max()); + BookTypesetter::Ptr typesetter = BookTypesetter::create (mHistory->getWidth(), std::numeric_limits::max()); for (std::vector::iterator it = mHistoryContents.begin(); it != mHistoryContents.end(); ++it) (*it)->write(typesetter, &mKeywordSearch, mTopicLinks); @@ -482,10 +513,10 @@ namespace MWGui typesetter->sectionBreak(9); // choices - const MyGUI::Colour linkHot (223/255.f, 201/255.f, 159/255.f); - const MyGUI::Colour linkNormal (150/255.f, 50/255.f, 30/255.f); - const MyGUI::Colour linkActive (243/255.f, 237/255.f, 221/255.f); - for (std::map::reverse_iterator it = mChoices.rbegin(); it != mChoices.rend(); ++it) + const MyGUI::Colour linkHot (getTextColour("answer_over")); + const MyGUI::Colour linkNormal (getTextColour("answer")); + const MyGUI::Colour linkActive (getTextColour("answer_pressed")); + for (std::vector >::iterator it = mChoices.begin(); it != mChoices.end(); ++it) { Choice* link = new Choice(it->second); mLinks.push_back(link); @@ -498,9 +529,11 @@ namespace MWGui if (mGoodbye) { + Goodbye* link = new Goodbye(); + mLinks.push_back(link); std::string goodbye = MWBase::Environment::get().getWorld()->getStore().get().find("sGoodbye")->getString(); BookTypesetter::Style* questionStyle = typesetter->createHotStyle(body, linkNormal, linkHot, linkActive, - TypesetBook::InteractiveId(mLinks.back())); + TypesetBook::InteractiveId(link)); typesetter->lineBreak(); typesetter->write(questionStyle, to_utf8_span(goodbye.c_str())); } @@ -527,12 +560,11 @@ namespace MWGui MyGUI::Button* byeButton; getWidget(byeButton, "ByeButton"); - if(MWBase::Environment::get().getDialogueManager()->isInChoice() && !mGoodbye) { - byeButton->setEnabled(false); - } - else { - byeButton->setEnabled(true); - } + bool goodbyeEnabled = !MWBase::Environment::get().getDialogueManager()->isInChoice() || mGoodbye; + byeButton->setEnabled(goodbyeEnabled); + + bool topicsEnabled = !MWBase::Environment::get().getDialogueManager()->isInChoice() && !mGoodbye; + mTopicsList->setEnabled(topicsEnabled); } void DialogueWindow::notifyLinkClicked (TypesetBook::InteractiveId link) @@ -576,7 +608,7 @@ namespace MWGui void DialogueWindow::addChoice(const std::string& choice, int id) { - mChoices[choice] = id; + mChoices.push_back(std::make_pair(choice, id)); updateHistory(); } @@ -597,8 +629,7 @@ namespace MWGui dispositionVisible = true; mDispositionBar->setProgressRange(100); mDispositionBar->setProgressPosition(MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(mPtr)); - mDispositionText->eraseText(0, mDispositionText->getTextLength()); - mDispositionText->addText("#B29154"+boost::lexical_cast(MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(mPtr))+std::string("/100")+"#B29154"); + mDispositionText->setCaption(MyGUI::utility::toString(MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(mPtr))+std::string("/100")); } bool dispositionWasVisible = mDispositionBar->getVisible(); @@ -621,9 +652,7 @@ namespace MWGui void DialogueWindow::goodbye() { - mLinks.push_back(new Goodbye()); mGoodbye = true; - mTopicsList->setEnabled(false); mEnabled = false; updateHistory(); } @@ -642,8 +671,7 @@ namespace MWGui + MWBase::Environment::get().getDialogueManager()->getTemporaryDispositionChange())); mDispositionBar->setProgressRange(100); mDispositionBar->setProgressPosition(disp); - mDispositionText->eraseText(0, mDispositionText->getTextLength()); - mDispositionText->addText("#B29154"+boost::lexical_cast(disp)+std::string("/100")+"#B29154"); + mDispositionText->setCaption(MyGUI::utility::toString(disp)+std::string("/100")); } } } diff --git a/apps/openmw/mwgui/dialogue.hpp b/apps/openmw/mwgui/dialogue.hpp index 516c04942..769c32af7 100644 --- a/apps/openmw/mwgui/dialogue.hpp +++ b/apps/openmw/mwgui/dialogue.hpp @@ -6,23 +6,18 @@ #include "bookpage.hpp" -#include "keywordsearch.hpp" +#include "../mwdialogue/keywordsearch.hpp" + +namespace Gui +{ + class MWList; +} namespace MWGui { class WindowManager; - - namespace Widgets - { - class MWList; - } } -/* - This file contains the dialouge window - Layout is defined by resources/mygui/openmw_dialogue_window.layout. - */ - namespace MWGui { class DialogueHistoryViewModel; @@ -76,7 +71,7 @@ namespace MWGui virtual void activated (); }; - typedef KeywordSearch KeywordSearchT; + typedef MWDialogue::KeywordSearch KeywordSearchT; struct DialogueText { @@ -152,6 +147,7 @@ namespace MWGui private: void updateOptions(); + void restock(); int mServices; @@ -160,7 +156,7 @@ namespace MWGui bool mGoodbye; std::vector mHistoryContents; - std::map mChoices; + std::vector > mChoices; std::vector mLinks; std::map mTopicLinks; @@ -168,9 +164,9 @@ namespace MWGui KeywordSearchT mKeywordSearch; BookPage* mHistory; - Widgets::MWList* mTopicsList; + Gui::MWList* mTopicsList; MyGUI::ScrollBar* mScrollBar; - MyGUI::Progress* mDispositionBar; + MyGUI::ProgressBar* mDispositionBar; MyGUI::EditBox* mDispositionText; PersuasionDialog mPersuasionDialog; diff --git a/apps/openmw/mwgui/draganddrop.cpp b/apps/openmw/mwgui/draganddrop.cpp new file mode 100644 index 000000000..fcb381b95 --- /dev/null +++ b/apps/openmw/mwgui/draganddrop.cpp @@ -0,0 +1,134 @@ +#include "draganddrop.hpp" + +#include +#include + +#include "../mwbase/windowmanager.hpp" +#include "../mwbase/environment.hpp" +#include "../mwbase/soundmanager.hpp" + +#include "../mwworld/class.hpp" + +#include "sortfilteritemmodel.hpp" +#include "inventorywindow.hpp" +#include "itemwidget.hpp" +#include "itemview.hpp" +#include "controllers.hpp" + +namespace MWGui +{ + + +DragAndDrop::DragAndDrop() + : mDraggedWidget(NULL) + , mDraggedCount(0) + , mSourceModel(NULL) + , mSourceView(NULL) + , mSourceSortModel(NULL) + , mIsOnDragAndDrop(false) +{ +} + +void DragAndDrop::startDrag (int index, SortFilterItemModel* sortModel, ItemModel* sourceModel, ItemView* sourceView, int count) +{ + mItem = sourceModel->getItem(index); + mDraggedCount = count; + mSourceModel = sourceModel; + mSourceView = sourceView; + mSourceSortModel = sortModel; + mIsOnDragAndDrop = true; + + // If picking up an item that isn't from the player's inventory, the item gets added to player inventory backend + // immediately, even though it's still floating beneath the mouse cursor. A bit counterintuitive, + // but this is how it works in vanilla, and not doing so would break quests (BM_beasts for instance). + ItemModel* playerModel = MWBase::Environment::get().getWindowManager()->getInventoryWindow()->getModel(); + if (mSourceModel != playerModel) + { + MWWorld::Ptr item = mSourceModel->moveItem(mItem, mDraggedCount, playerModel); + + playerModel->update(); + + ItemModel::ModelIndex newIndex = -1; + for (unsigned int i=0; igetItemCount(); ++i) + { + if (playerModel->getItem(i).mBase == item) + { + newIndex = i; + break; + } + } + mItem = playerModel->getItem(newIndex); + mSourceModel = playerModel; + + SortFilterItemModel* playerFilterModel = MWBase::Environment::get().getWindowManager()->getInventoryWindow()->getSortFilterModel(); + mSourceSortModel = playerFilterModel; + } + + std::string sound = mItem.mBase.getClass().getUpSoundId(mItem.mBase); + MWBase::Environment::get().getSoundManager()->playSound (sound, 1.0, 1.0); + + if (mSourceSortModel) + { + mSourceSortModel->clearDragItems(); + mSourceSortModel->addDragItem(mItem.mBase, count); + } + + ItemWidget* baseWidget = MyGUI::Gui::getInstance().createWidget("MW_ItemIcon", 0, 0, 42, 42, MyGUI::Align::Default, "DragAndDrop"); + + Controllers::ControllerFollowMouse* controller = + MyGUI::ControllerManager::getInstance().createItem(Controllers::ControllerFollowMouse::getClassTypeName()) + ->castType(); + MyGUI::ControllerManager::getInstance().addItem(baseWidget, controller); + + mDraggedWidget = baseWidget; + baseWidget->setItem(mItem.mBase); + baseWidget->setNeedMouseFocus(false); + baseWidget->setCount(count); + + sourceView->update(); + + MWBase::Environment::get().getWindowManager()->setDragDrop(true); +} + +void DragAndDrop::drop(ItemModel *targetModel, ItemView *targetView) +{ + std::string sound = mItem.mBase.getClass().getDownSoundId(mItem.mBase); + MWBase::Environment::get().getSoundManager()->playSound (sound, 1.0, 1.0); + + // We can't drop a conjured item to the ground; the target container should always be the source container + if (mItem.mFlags & ItemStack::Flag_Bound && targetModel != mSourceModel) + { + MWBase::Environment::get().getWindowManager()->messageBox("#{sBarterDialog12}"); + return; + } + + // If item is dropped where it was taken from, we don't need to do anything - + // otherwise, do the transfer + if (targetModel != mSourceModel) + { + mSourceModel->moveItem(mItem, mDraggedCount, targetModel); + } + + mSourceModel->update(); + + finish(); + if (targetView) + targetView->update(); + + MWBase::Environment::get().getWindowManager()->getInventoryWindow()->updateItemView(); + + // We need to update the view since an other item could be auto-equipped. + mSourceView->update(); +} + +void DragAndDrop::finish() +{ + mIsOnDragAndDrop = false; + mSourceSortModel->clearDragItems(); + + MyGUI::Gui::getInstance().destroyWidget(mDraggedWidget); + mDraggedWidget = 0; + MWBase::Environment::get().getWindowManager()->setDragDrop(false); +} + +} diff --git a/apps/openmw/mwgui/draganddrop.hpp b/apps/openmw/mwgui/draganddrop.hpp new file mode 100644 index 000000000..a356fe4e2 --- /dev/null +++ b/apps/openmw/mwgui/draganddrop.hpp @@ -0,0 +1,38 @@ +#ifndef OPENMW_MWGUI_DRAGANDDROP_H +#define OPENMW_MWGUI_DRAGANDDROP_H + +#include "itemmodel.hpp" + +namespace MyGUI +{ + class Widget; +} + +namespace MWGui +{ + + class ItemView; + class SortFilterItemModel; + + class DragAndDrop + { + public: + bool mIsOnDragAndDrop; + MyGUI::Widget* mDraggedWidget; + ItemModel* mSourceModel; + ItemView* mSourceView; + SortFilterItemModel* mSourceSortModel; + ItemStack mItem; + int mDraggedCount; + + DragAndDrop(); + + void startDrag (int index, SortFilterItemModel* sortModel, ItemModel* sourceModel, ItemView* sourceView, int count); + void drop (ItemModel* targetModel, ItemView* targetView); + + void finish(); + }; + +} + +#endif diff --git a/apps/openmw/mwgui/enchantingdialog.cpp b/apps/openmw/mwgui/enchantingdialog.cpp index 92221977b..43f2493a9 100644 --- a/apps/openmw/mwgui/enchantingdialog.cpp +++ b/apps/openmw/mwgui/enchantingdialog.cpp @@ -4,6 +4,12 @@ #include +#include +#include + +#include +#include + #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/soundmanager.hpp" @@ -11,9 +17,9 @@ #include "../mwbase/mechanicsmanager.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" +#include "../mwworld/esmstore.hpp" #include "itemselection.hpp" -#include "container.hpp" #include "itemwidget.hpp" #include "sortfilteritemmodel.hpp" @@ -24,7 +30,7 @@ namespace MWGui EnchantingDialog::EnchantingDialog() : WindowBase("openmw_enchanting_dialog.layout") - , EffectEditorBase() + , EffectEditorBase(EffectEditorBase::Enchanting) , mItemSelectionDialog(NULL) { getWidget(mName, "NameEdit"); @@ -58,9 +64,6 @@ namespace MWGui void EnchantingDialog::open() { center(); - - setSoulGem(MWWorld::Ptr()); - setItem(MWWorld::Ptr()); } void EnchantingDialog::setSoulGem(const MWWorld::Ptr &gem) @@ -78,7 +81,6 @@ namespace MWGui mSoulBox->setUserData(gem); mEnchanting.setSoulGem(gem); } - updateLabels(); } void EnchantingDialog::setItem(const MWWorld::Ptr &item) @@ -91,12 +93,12 @@ namespace MWGui } else { + mName->setCaption(item.getClass().getName(item)); mItemBox->setItem(item); mItemBox->setUserString ("ToolTipType", "ItemPtr"); mItemBox->setUserData(item); mEnchanting.setOldItem(item); } - updateLabels(); } void EnchantingDialog::exit() @@ -108,33 +110,33 @@ namespace MWGui { std::stringstream enchantCost; enchantCost << std::setprecision(1) << std::fixed << mEnchanting.getEnchantPoints(); - mEnchantmentPoints->setCaption(enchantCost.str() + " / " + boost::lexical_cast(mEnchanting.getMaxEnchantValue())); + mEnchantmentPoints->setCaption(enchantCost.str() + " / " + MyGUI::utility::toString(mEnchanting.getMaxEnchantValue())); - mCharge->setCaption(boost::lexical_cast(mEnchanting.getGemCharge())); + mCharge->setCaption(MyGUI::utility::toString(mEnchanting.getGemCharge())); std::stringstream castCost; - castCost << std::setprecision(1) << std::fixed << mEnchanting.getCastCost(); - mCastCost->setCaption(boost::lexical_cast(castCost.str())); + castCost << mEnchanting.getEffectiveCastCost(); + mCastCost->setCaption(castCost.str()); - mPrice->setCaption(boost::lexical_cast(mEnchanting.getEnchantPrice())); + mPrice->setCaption(MyGUI::utility::toString(mEnchanting.getEnchantPrice())); switch(mEnchanting.getCastStyle()) { case ESM::Enchantment::CastOnce: mTypeButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sItemCastOnce","Cast Once")); - mAddEffectDialog.constantEffect=false; + setConstantEffect(false); break; case ESM::Enchantment::WhenStrikes: mTypeButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sItemCastWhenStrikes", "When Strikes")); - mAddEffectDialog.constantEffect=false; + setConstantEffect(false); break; case ESM::Enchantment::WhenUsed: mTypeButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sItemCastWhenUsed", "When Used")); - mAddEffectDialog.constantEffect=false; + setConstantEffect(false); break; case ESM::Enchantment::ConstantEffect: mTypeButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sItemCastConstant", "Cast Constant")); - mAddEffectDialog.constantEffect=true; + setConstantEffect(true); break; } } @@ -144,9 +146,17 @@ namespace MWGui mEnchanting.setSelfEnchanting(false); mEnchanting.setEnchanter(actor); + mBuyButton->setCaptionWithReplacing("#{sBuy}"); + mPtr = actor; + setSoulGem(MWWorld::Ptr()); + setItem(MWWorld::Ptr()); + startEditing (); + mPrice->setVisible(true); + mPriceText->setVisible(true); + updateLabels(); } void EnchantingDialog::startSelfEnchanting(MWWorld::Ptr soulgem) @@ -156,11 +166,13 @@ namespace MWGui mEnchanting.setSelfEnchanting(true); mEnchanting.setEnchanter(player); + mBuyButton->setCaptionWithReplacing("#{sCreate}"); + mPtr = player; startEditing(); - mEnchanting.setSoulGem(soulgem); setSoulGem(soulgem); + setItem(MWWorld::Ptr()); mPrice->setVisible(false); mPriceText->setVisible(false); @@ -171,6 +183,16 @@ namespace MWGui { MWBase::Environment::get().getWindowManager()->removeGuiMode (GM_Dialogue); MWBase::Environment::get().getWindowManager()->removeGuiMode (GM_Enchanting); + resetReference(); + } + + void EnchantingDialog::resetReference() + { + ReferenceInterface::resetReference(); + setItem(MWWorld::Ptr()); + setSoulGem(MWWorld::Ptr()); + mPtr = MWWorld::Ptr(); + mEnchanting.setEnchanter(MWWorld::Ptr()); } void EnchantingDialog::onCancelButtonClicked(MyGUI::Widget* sender) @@ -193,6 +215,7 @@ namespace MWGui else { setItem(MWWorld::Ptr()); + updateLabels(); } } @@ -201,6 +224,7 @@ namespace MWGui mItemSelectionDialog->setVisible(false); setItem(item); + MWBase::Environment::get().getSoundManager()->playSound(item.getClass().getDownSoundId(item), 1, 1); mEnchanting.nextCastStyle(); updateLabels(); } @@ -213,8 +237,8 @@ namespace MWGui void EnchantingDialog::onSoulSelected(MWWorld::Ptr item) { mItemSelectionDialog->setVisible(false); - mEnchanting.setSoulGem(item); + mEnchanting.setSoulGem(item); if(mEnchanting.getGemCharge()==0) { MWBase::Environment::get().getWindowManager()->messageBox ("#{sNotifyMessage32}"); @@ -222,6 +246,8 @@ namespace MWGui } setSoulGem(item); + MWBase::Environment::get().getSoundManager()->playSound(item.getClass().getDownSoundId(item), 1, 1); + updateLabels(); } void EnchantingDialog::onSoulCancel() @@ -246,6 +272,7 @@ namespace MWGui else { setSoulGem(MWWorld::Ptr()); + updateLabels(); } } @@ -260,6 +287,7 @@ namespace MWGui { mEnchanting.nextCastStyle(); updateLabels(); + updateEffectsView(); } void EnchantingDialog::onBuyButtonClicked(MyGUI::Widget* sender) @@ -299,7 +327,7 @@ namespace MWGui MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); int playerGold = player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId); - if (mEnchanting.getEnchantPrice() > playerGold) + if (mPtr != player && mEnchanting.getEnchantPrice() > playerGold) { MWBase::Environment::get().getWindowManager()->messageBox ("#{sNotifyMessage18}"); return; @@ -311,17 +339,17 @@ namespace MWGui for (int i=0; i<2; ++i) { MWWorld::Ptr item = (i == 0) ? mEnchanting.getOldItem() : mEnchanting.getGem(); - if (Misc::StringUtils::ciEqual(item.getCellRef().getOwner(), mPtr.getCellRef().getRefId())) + if (MWBase::Environment::get().getMechanicsManager()->isItemStolenFrom(item.getCellRef().getRefId(), + mPtr.getCellRef().getRefId())) { std::string msg = MWBase::Environment::get().getWorld()->getStore().get().find("sNotifyMessage49")->getString(); if (msg.find("%s") != std::string::npos) msg.replace(msg.find("%s"), 2, item.getClass().getName(item)); MWBase::Environment::get().getWindowManager()->messageBox(msg); - MWBase::Environment::get().getDialogueManager()->say(mPtr, "Thief"); - MWBase::Environment::get().getMechanicsManager()->reportCrime(player, mPtr, MWBase::MechanicsManager::OT_Theft, - item.getClass().getValue(item)); + MWBase::Environment::get().getMechanicsManager()->commitCrime(player, mPtr, MWBase::MechanicsManager::OT_Theft, + item.getClass().getValue(item), true); MWBase::Environment::get().getWindowManager()->removeGuiMode (GM_Enchanting); - MWBase::Environment::get().getWindowManager()->removeGuiMode (GM_Dialogue); + MWBase::Environment::get().getDialogueManager()->goodbyeSelected(); return; } } diff --git a/apps/openmw/mwgui/enchantingdialog.hpp b/apps/openmw/mwgui/enchantingdialog.hpp index b75ae8280..5b67d199b 100644 --- a/apps/openmw/mwgui/enchantingdialog.hpp +++ b/apps/openmw/mwgui/enchantingdialog.hpp @@ -29,6 +29,8 @@ namespace MWGui void startEnchanting(MWWorld::Ptr actor); void startSelfEnchanting(MWWorld::Ptr soulgem); + virtual void resetReference(); + protected: virtual void onReferenceUnavailable(); virtual void notifyEffectsChanged (); diff --git a/apps/openmw/mwgui/fontloader.hpp b/apps/openmw/mwgui/fontloader.hpp deleted file mode 100644 index 7954b0875..000000000 --- a/apps/openmw/mwgui/fontloader.hpp +++ /dev/null @@ -1,25 +0,0 @@ -#ifndef MWGUI_FONTLOADER_H -#define MWGUI_FONTLOADER_H - -#include - -namespace MWGui -{ - - - /// @brief loads Morrowind's .fnt/.tex fonts for use with MyGUI and Ogre - class FontLoader - { - public: - FontLoader (ToUTF8::FromType encoding); - void loadAllFonts (); - - private: - ToUTF8::FromType mEncoding; - - void loadFont (const std::string& fileName); - }; - -} - -#endif diff --git a/apps/openmw/mwgui/formatting.cpp b/apps/openmw/mwgui/formatting.cpp index 4d3d04ced..761a89ca6 100644 --- a/apps/openmw/mwgui/formatting.cpp +++ b/apps/openmw/mwgui/formatting.cpp @@ -1,419 +1,487 @@ #include "formatting.hpp" -#include +#include +#include -#include "../mwscript/interpretercontext.hpp" +#include +#include +#include +#include +#include #include #include -#include -#include -#include -#include -#include -#include +#include +#include +#include + +#include "../mwscript/interpretercontext.hpp" -namespace +namespace MWGui { - int convertFromHex(std::string hex) + namespace Formatting { - int value = 0; + /* BookTextParser */ + BookTextParser::BookTextParser(const std::string & text) + : mIndex(0), mText(text), mIgnoreNewlineTags(true), mIgnoreLineEndings(true), mClosingTag(false) + { + MWScript::InterpreterContext interpreterContext(NULL, MWWorld::Ptr()); // empty arguments, because there is no locals or actor + mText = Interpreter::fixDefinesBook(mText, interpreterContext); - int a = 0; - int b = hex.length() - 1; - for (; b >= 0; a++, b--) + boost::algorithm::replace_all(mText, "\r", ""); + + registerTag("br", Event_BrTag); + registerTag("p", Event_PTag); + registerTag("img", Event_ImgTag); + registerTag("div", Event_DivTag); + registerTag("font", Event_FontTag); + } + + void BookTextParser::registerTag(const std::string & tag, BookTextParser::Events type) { - if (hex[b] >= '0' && hex[b] <= '9') - { - value += (hex[b] - '0') * (1 << (a * 4)); - } - else + mTagTypes[tag] = type; + } + + std::string BookTextParser::getReadyText() const + { + return mReadyText; + } + + BookTextParser::Events BookTextParser::next() + { + while (mIndex < mText.size()) { - switch (hex[b]) + char ch = mText[mIndex]; + if (ch == '<') { - case 'A': - case 'a': - value += 10 * (1 << (a * 4)); - break; + const size_t tagStart = mIndex + 1; + const size_t tagEnd = mText.find('>', tagStart); + if (tagEnd == std::string::npos) + throw std::runtime_error("BookTextParser Error: Tag is not terminated"); + parseTag(mText.substr(tagStart, tagEnd - tagStart)); + mIndex = tagEnd; - case 'B': - case 'b': - value += 11 * (1 << (a * 4)); - break; + if (mTagTypes.find(mTag) != mTagTypes.end()) + { + Events type = mTagTypes.at(mTag); + + if (type == Event_BrTag || type == Event_PTag) + { + if (!mIgnoreNewlineTags) + { + if (type == Event_BrTag) + mBuffer.push_back('\n'); + else + { + mBuffer.append("\n\n"); + } + } + mIgnoreLineEndings = true; + } + else + flushBuffer(); - case 'C': - case 'c': - value += 12 * (1 << (a * 4)); - break; + if (type == Event_ImgTag) + { + mIgnoreNewlineTags = false; + } - case 'D': - case 'd': - value += 13 * (1 << (a * 4)); - break; + ++mIndex; + return type; + } + } + else + { + if (!mIgnoreLineEndings || ch != '\n') + { + mBuffer.push_back(ch); + mIgnoreLineEndings = false; + mIgnoreNewlineTags = false; + } + } - case 'E': - case 'e': - value += 14 * (1 << (a * 4)); - break; + ++mIndex; + } - case 'F': - case 'f': - value += 15 * (1 << (a * 4)); - break; + flushBuffer(); + return Event_EOF; + } - default: - throw std::runtime_error("invalid character in hex number"); - break; - } - } + void BookTextParser::flushBuffer() + { + mReadyText = mBuffer; + mBuffer.clear(); } - return value; - } + const BookTextParser::Attributes & BookTextParser::getAttributes() const + { + return mAttributes; + } - Ogre::UTFString::unicode_char unicodeCharFromChar(char ch) - { - std::string s; - s += ch; - Ogre::UTFString string(s); - return string.getChar(0); - } - - bool is_not_empty(const std::string& s) { - std::string temp = s; - boost::algorithm::trim(temp); - return !temp.empty(); - } -} + bool BookTextParser::isClosingTag() const + { + return mClosingTag; + } -namespace MWGui -{ + void BookTextParser::parseTag(std::string tag) + { + size_t tagNameEndPos = tag.find(' '); + mAttributes.clear(); + mTag = tag.substr(0, tagNameEndPos); + Misc::StringUtils::toLower(mTag); + if (mTag.empty()) + return; + + mClosingTag = (mTag[0] == '/'); + if (mClosingTag) + { + mTag.erase(mTag.begin()); + return; + } - std::vector BookTextParser::split(std::string utf8Text, const int width, const int height) - { - using Ogre::UTFString; - std::vector result; + if (tagNameEndPos == std::string::npos) + return; + tag.erase(0, tagNameEndPos+1); + + while (!tag.empty()) + { + size_t sepPos = tag.find('='); + if (sepPos == std::string::npos) + return; + + std::string key = tag.substr(0, sepPos); + Misc::StringUtils::toLower(key); + tag.erase(0, sepPos+1); - MWScript::InterpreterContext interpreterContext(NULL, MWWorld::Ptr()); // empty arguments, because there is no locals or actor - utf8Text = Interpreter::fixDefinesBook(utf8Text, interpreterContext); + std::string value; - boost::algorithm::replace_all(utf8Text, "\n", ""); - boost::algorithm::replace_all(utf8Text, "\r", ""); - boost::algorithm::replace_all(utf8Text, "
    ", "\n"); - boost::algorithm::replace_all(utf8Text, "

    ", "\n\n"); + if (tag.empty()) + return; - UTFString text(utf8Text); - const int spacing = 48; + if (tag[0] == '"') + { + size_t quoteEndPos = tag.find('"', 1); + if (quoteEndPos == std::string::npos) + throw std::runtime_error("BookTextParser Error: Missing end quote in tag"); + value = tag.substr(1, quoteEndPos-1); + tag.erase(0, quoteEndPos+2); + } + else + { + size_t valEndPos = tag.find(' '); + if (valEndPos == std::string::npos) + { + value = tag; + tag.erase(); + } + else + { + value = tag.substr(0, valEndPos); + tag.erase(0, valEndPos+1); + } + } - const UTFString::unicode_char LEFT_ANGLE = unicodeCharFromChar('<'); - const UTFString::unicode_char NEWLINE = unicodeCharFromChar('\n'); - const UTFString::unicode_char SPACE = unicodeCharFromChar(' '); + mAttributes[key] = value; + } + } - while (!text.empty()) + /* BookFormatter */ + Paginator::Pages BookFormatter::markupToWidget(MyGUI::Widget * parent, const std::string & markup, const int pageWidth, const int pageHeight) { - // read in characters until we have exceeded the size, or run out of text - int currentWidth = 0; - int currentHeight = 0; + Paginator pag(pageWidth, pageHeight); - size_t currentWordStart = 0; - size_t index = 0; - + while (parent->getChildCount()) { - std::string texToTrim = text.asUTF8(); - boost::algorithm::trim( texToTrim ); - text = UTFString(texToTrim); + MyGUI::Gui::getInstance().destroyWidget(parent->getChildAt(0)); } - - - while (currentHeight <= height - spacing && index < text.size()) + + mTextStyle = TextStyle(); + mBlockStyle = BlockStyle(); + + MyGUI::Widget * paper = parent->createWidget("Widget", MyGUI::IntCoord(0, 0, pag.getPageWidth(), pag.getPageHeight()), MyGUI::Align::Left | MyGUI::Align::Top); + paper->setNeedMouseFocus(false); + + BookTextParser parser(markup); + + bool brBeforeLastTag = false; + bool isPrevImg = false; + for (;;) { - const UTFString::unicode_char ch = text.getChar(index); - if (ch == LEFT_ANGLE) - { - const size_t tagStart = index + 1; - const size_t tagEnd = text.find('>', tagStart); - if (tagEnd == UTFString::npos) - throw std::runtime_error("BookTextParser Error: Tag is not terminated"); - const std::string tag = text.substr(tagStart, tagEnd - tagStart).asUTF8(); + BookTextParser::Events event = parser.next(); + if (event == BookTextParser::Event_BrTag || event == BookTextParser::Event_PTag) + continue; - if (boost::algorithm::starts_with(tag, "IMG")) - { - const int h = mHeight; - parseImage(tag, false); - currentHeight += (mHeight - h); - currentWidth = 0; - } - else if (boost::algorithm::starts_with(tag, "FONT")) - { - parseFont(tag); - if (currentWidth != 0) { - currentHeight += currentFontHeight(); - currentWidth = 0; - } - currentWidth = 0; - } - else if (boost::algorithm::starts_with(tag, "DIV")) + std::string plainText = parser.getReadyText(); + + // for cases when linebreaks are used to cause a shift to the next page + // if the split text block ends in an empty line, proceeding text block(s) should have leading empty lines removed + if (pag.getIgnoreLeadingEmptyLines()) + { + while (!plainText.empty()) { - parseDiv(tag); - if (currentWidth != 0) { - currentHeight += currentFontHeight(); - currentWidth = 0; + if (plainText[0] == '\n') + plainText.erase(plainText.begin()); + else + { + pag.setIgnoreLeadingEmptyLines(false); + break; } } - index = tagEnd; - } - else if (ch == NEWLINE) - { - currentHeight += currentFontHeight(); - currentWidth = 0; - currentWordStart = index; - } - else if (ch == SPACE) - { - currentWidth += 3; // keep this in sync with the font's SpaceWidth property - currentWordStart = index; } + + if (plainText.empty()) + brBeforeLastTag = true; else { - currentWidth += widthForCharGlyph(ch); - } + // Each block of text (between two tags / boundary and tag) will be displayed in a separate editbox widget, + // which means an additional linebreak will be created between them. + // ^ This is not what vanilla MW assumes, so we must deal with line breaks around tags appropriately. + bool brAtStart = (plainText[0] == '\n'); + bool brAtEnd = (plainText[plainText.size()-1] == '\n'); + + if (brAtStart && !brBeforeLastTag && !isPrevImg) + plainText.erase(plainText.begin()); + + if (plainText.size() && brAtEnd) + plainText.erase(plainText.end()-1); + +#if (MYGUI_VERSION < MYGUI_DEFINE_VERSION(3, 2, 2)) + // splitting won't be fully functional until 3.2.2 (see TextElement::pageSplit()) + // hack: prevent newlines at the end of the book possibly creating unnecessary pages + if (event == BookTextParser::Event_EOF) + { + while (plainText.size() && plainText[plainText.size()-1] == '\n') + plainText.erase(plainText.end()-1); + } +#endif - if (currentWidth > width) - { - currentHeight += currentFontHeight(); - currentWidth = 0; - // add size of the current word - UTFString word = text.substr(currentWordStart, index - currentWordStart); - for (UTFString::const_iterator it = word.begin(), end = word.end(); it != end; ++it) - currentWidth += widthForCharGlyph(it.getCharacter()); + if (!plainText.empty() || brBeforeLastTag || isPrevImg) + { + TextElement elem(paper, pag, mBlockStyle, + mTextStyle, plainText); + elem.paginate(); + } + + brBeforeLastTag = brAtEnd; } - index += UTFString::_utf16_char_length(ch); - } - const size_t pageEnd = (currentHeight > height - spacing && currentWordStart != 0) - ? currentWordStart : index; - result.push_back(text.substr(0, pageEnd).asUTF8()); - text.erase(0, pageEnd); - } - - std::vector nonEmptyPages; - boost::copy(result | boost::adaptors::filtered(is_not_empty), std::back_inserter(nonEmptyPages)); - return nonEmptyPages; - } + if (event == BookTextParser::Event_EOF) + break; - float BookTextParser::widthForCharGlyph(unsigned unicodeChar) const - { - std::string fontName(mTextStyle.mFont == "Default" ? MyGUI::FontManager::getInstance().getDefaultFont() : mTextStyle.mFont); - return MyGUI::FontManager::getInstance().getByName(fontName) - ->getGlyphInfo(unicodeChar)->width; - } + isPrevImg = (event == BookTextParser::Event_ImgTag); - float BookTextParser::currentFontHeight() const - { - std::string fontName(mTextStyle.mFont == "Default" ? MyGUI::FontManager::getInstance().getDefaultFont() : mTextStyle.mFont); - return MyGUI::FontManager::getInstance().getByName(fontName)->getDefaultHeight(); - } + switch (event) + { + case BookTextParser::Event_ImgTag: + { + pag.setIgnoreLeadingEmptyLines(false); - MyGUI::IntSize BookTextParser::parsePage(std::string text, MyGUI::Widget* parent, const int width) - { - MWScript::InterpreterContext interpreterContext(NULL, MWWorld::Ptr()); // empty arguments, because there is no locals or actor - text = Interpreter::fixDefinesBook(text, interpreterContext); + const BookTextParser::Attributes & attr = parser.getAttributes(); - mParent = parent; - mWidth = width; - mHeight = 0; + if (attr.find("src") == attr.end() || attr.find("width") == attr.end() || attr.find("height") == attr.end()) + continue; - assert(mParent); - while (mParent->getChildCount()) - { - MyGUI::Gui::getInstance().destroyWidget(mParent->getChildAt(0)); - } + std::string src = attr.at("src"); + int width = MyGUI::utility::parseInt(attr.at("width")); + int height = MyGUI::utility::parseInt(attr.at("height")); - // remove trailing " - if (text[text.size()-1] == '\"') - text.erase(text.size()-1); + ImageElement elem(paper, pag, mBlockStyle, + src, width, height); + elem.paginate(); + break; + } + case BookTextParser::Event_FontTag: + if (parser.isClosingTag()) + resetFontProperties(); + else + handleFont(parser.getAttributes()); + break; + case BookTextParser::Event_DivTag: + handleDiv(parser.getAttributes()); + break; + default: + break; + } + } - parseSubText(text); - return MyGUI::IntSize(mWidth, mHeight); - } - - MyGUI::IntSize BookTextParser::parseScroll(std::string text, MyGUI::Widget* parent, const int width) - { - MWScript::InterpreterContext interpreterContext(NULL, MWWorld::Ptr()); // empty arguments, because there is no locals or actor - text = Interpreter::fixDefinesBook(text, interpreterContext); + // insert last page + if (pag.getStartTop() != pag.getCurrentTop()) + pag << Paginator::Page(pag.getStartTop(), pag.getStartTop() + pag.getPageHeight()); + + paper->setSize(paper->getWidth(), pag.getCurrentTop()); - mParent = parent; - mWidth = width; - mHeight = 0; + return pag.getPages(); + } - assert(mParent); - while (mParent->getChildCount()) + Paginator::Pages BookFormatter::markupToWidget(MyGUI::Widget * parent, const std::string & markup) { - MyGUI::Gui::getInstance().destroyWidget(mParent->getChildAt(0)); + return markupToWidget(parent, markup, parent->getWidth(), parent->getHeight()); } - boost::algorithm::replace_all(text, "
    ", "\n"); - boost::algorithm::replace_all(text, "

    ", "\n\n"); - boost::algorithm::trim_left(text); + void BookFormatter::resetFontProperties() + { + mTextStyle = TextStyle(); + } - // remove trailing " - if (text[text.size()-1] == '\"') - text.erase(text.size()-1); + void BookFormatter::handleDiv(const BookTextParser::Attributes & attr) + { + if (attr.find("align") == attr.end()) + return; - parseSubText(text); - return MyGUI::IntSize(mWidth, mHeight); - } - + std::string align = attr.at("align"); - void BookTextParser::parseImage(std::string tag, bool createWidget) - { - int src_start = tag.find("SRC=")+5; - std::string image = tag.substr(src_start, tag.find('"', src_start)-src_start); + if (Misc::StringUtils::ciEqual(align, "center")) + mBlockStyle.mAlign = MyGUI::Align::HCenter; + else if (Misc::StringUtils::ciEqual(align, "left")) + mBlockStyle.mAlign = MyGUI::Align::Left; + else if (Misc::StringUtils::ciEqual(align, "right")) + mBlockStyle.mAlign = MyGUI::Align::Right; + } - // fix texture extension to .dds - if (image.size() > 4) + void BookFormatter::handleFont(const BookTextParser::Attributes & attr) { - image[image.size()-3] = 'd'; - image[image.size()-2] = 'd'; - image[image.size()-1] = 's'; + if (attr.find("color") != attr.end()) + { + unsigned int color; + std::stringstream ss; + ss << attr.at("color"); + ss >> std::hex >> color; + + mTextStyle.mColour = MyGUI::Colour( + (color>>16 & 0xFF) / 255.f, + (color>>8 & 0xFF) / 255.f, + (color & 0xFF) / 255.f); + } + if (attr.find("face") != attr.end()) + { + std::string face = attr.at("face"); + mTextStyle.mFont = face; + } + if (attr.find("size") != attr.end()) + { + /// \todo + } } - int width_start = tag.find("WIDTH=")+7; - int width = boost::lexical_cast(tag.substr(width_start, tag.find('"', width_start)-width_start)); - - int height_start = tag.find("HEIGHT=")+8; - int height = boost::lexical_cast(tag.substr(height_start, tag.find('"', height_start)-height_start)); - - if (createWidget) + /* GraphicElement */ + GraphicElement::GraphicElement(MyGUI::Widget * parent, Paginator & pag, const BlockStyle & blockStyle) + : mParent(parent), mPaginator(pag), mBlockStyle(blockStyle) { - MyGUI::ImageBox* box = mParent->createWidget ("ImageBox", - MyGUI::IntCoord(0, mHeight, width, height), MyGUI::Align::Left | MyGUI::Align::Top, - mParent->getName() + boost::lexical_cast(mParent->getChildCount())); + } - // Apparently a bug with some morrowind versions, they reference the image without the size suffix. - // So if the image isn't found, try appending the size. - if (!Ogre::ResourceGroupManager::getSingleton().resourceExistsInAnyGroup("bookart\\"+image)) + void GraphicElement::paginate() + { + int newTop = mPaginator.getCurrentTop() + getHeight(); + while (newTop-mPaginator.getStartTop() > mPaginator.getPageHeight()) { - std::stringstream str; - str << image.substr(0, image.rfind(".")) << "_" << width << "_" << height << image.substr(image.rfind(".")); - image = str.str(); + int newStartTop = pageSplit(); + mPaginator << Paginator::Page(mPaginator.getStartTop(), newStartTop); + mPaginator.setStartTop(newStartTop); } - box->setImageTexture("bookart\\" + image); - box->setProperty("NeedMouse", "false"); + mPaginator.setCurrentTop(newTop); } - mWidth = std::max(mWidth, width); - mHeight += height; - } - - void BookTextParser::parseDiv(std::string tag) - { - if (tag.find("ALIGN=") == std::string::npos) - return; - - int align_start = tag.find("ALIGN=")+7; - std::string align = tag.substr(align_start, tag.find('"', align_start)-align_start); - if (align == "CENTER") - mTextStyle.mTextAlign = MyGUI::Align::HCenter; - else if (align == "LEFT") - mTextStyle.mTextAlign = MyGUI::Align::Left; - } - - void BookTextParser::parseFont(std::string tag) - { - if (tag.find("COLOR=") != std::string::npos) + int GraphicElement::pageSplit() { - int color_start = tag.find("COLOR=")+7; - std::string color = tag.substr(color_start, tag.find('"', color_start)-color_start); - - mTextStyle.mColour = MyGUI::Colour( - convertFromHex(color.substr(0, 2))/255.0, - convertFromHex(color.substr(2, 2))/255.0, - convertFromHex(color.substr(4, 2))/255.0); + return mPaginator.getStartTop() + mPaginator.getPageHeight(); } - if (tag.find("FACE=") != std::string::npos) - { - int face_start = tag.find("FACE=")+6; - std::string face = tag.substr(face_start, tag.find('"', face_start)-face_start); - if (face != "Magic Cards") - mTextStyle.mFont = face; + /* TextElement */ + TextElement::TextElement(MyGUI::Widget * parent, Paginator & pag, const BlockStyle & blockStyle, + const TextStyle & textStyle, const std::string & text) + : GraphicElement(parent, pag, blockStyle), + mTextStyle(textStyle) + { + MyGUI::EditBox* box = parent->createWidget("NormalText", + MyGUI::IntCoord(0, pag.getCurrentTop(), pag.getPageWidth(), 0), MyGUI::Align::Left | MyGUI::Align::Top, + parent->getName() + MyGUI::utility::toString(parent->getChildCount())); + box->setProperty("Static", "true"); + box->setProperty("MultiLine", "true"); + box->setProperty("WordWrap", "true"); + box->setProperty("NeedMouse", "false"); + box->setMaxTextLength(text.size()); + box->setTextAlign(mBlockStyle.mAlign); + box->setTextColour(mTextStyle.mColour); + box->setFontName(mTextStyle.mFont); + box->setCaption(MyGUI::TextIterator::toTagsString(text)); + box->setSize(box->getSize().width, box->getTextSize().height); + mEditBox = box; } - if (tag.find("SIZE=") != std::string::npos) + + int TextElement::currentFontHeight() const { - /// \todo + std::string fontName(mTextStyle.mFont == "Default" ? MyGUI::FontManager::getInstance().getDefaultFont() : mTextStyle.mFont); + return MyGUI::FontManager::getInstance().getByName(fontName)->getDefaultHeight(); } - } - void BookTextParser::parseSubText(std::string text) - { - if (text[0] == '<') + int TextElement::getHeight() { - const size_t tagStart = 1; - const size_t tagEnd = text.find('>', tagStart); - if (tagEnd == std::string::npos) - throw std::runtime_error("BookTextParser Error: Tag is not terminated"); - const std::string tag = text.substr(tagStart, tagEnd - tagStart); - - if (boost::algorithm::starts_with(tag, "IMG")) - parseImage(tag); - if (boost::algorithm::starts_with(tag, "FONT")) - parseFont(tag); - if (boost::algorithm::starts_with(tag, "DIV")) - parseDiv(tag); - - text.erase(0, tagEnd + 1); + return mEditBox->getTextSize().height; } - size_t tagStart = std::string::npos; - std::string realText; // real text, without tags - for (size_t i = 0; i= MYGUI_DEFINE_VERSION(3, 2, 2)) + mPaginator.setIgnoreLeadingEmptyLines(true); + + const MyGUI::VectorLineInfo & lines = mEditBox->getSubWidgetText()->castType()->getLineInfo(); + for (unsigned int i = lastLine; i < lines.size(); ++i) { - if ((i + 1 < text.size()) && text[i+1] == '/') // ignore closing tags - { - while (c != '>') - { - if (i >= text.size()) - throw std::runtime_error("BookTextParser Error: Tag is not terminated"); - ++i; - c = text[i]; - } - continue; - } + if (lines[i].width == 0) + ret += lineHeight; else { - tagStart = i; + mPaginator.setIgnoreLeadingEmptyLines(false); break; } } - else - realText += c; +#endif + return ret; } - MyGUI::EditBox* box = mParent->createWidget("NormalText", - MyGUI::IntCoord(0, mHeight, mWidth, 24), MyGUI::Align::Left | MyGUI::Align::Top, - mParent->getName() + boost::lexical_cast(mParent->getChildCount())); - box->setProperty("Static", "true"); - box->setProperty("MultiLine", "true"); - box->setProperty("WordWrap", "true"); - box->setProperty("NeedMouse", "false"); - box->setMaxTextLength(realText.size()); - box->setTextAlign(mTextStyle.mTextAlign); - box->setTextColour(mTextStyle.mColour); - box->setFontName(mTextStyle.mFont); - box->setCaption(realText); - box->setSize(box->getSize().width, box->getTextSize().height); - mHeight += box->getTextSize().height; - - if (tagStart != std::string::npos) + /* ImageElement */ + ImageElement::ImageElement(MyGUI::Widget * parent, Paginator & pag, const BlockStyle & blockStyle, + const std::string & src, int width, int height) + : GraphicElement(parent, pag, blockStyle), + mImageHeight(height) { - parseSubText(text.substr(tagStart, text.size())); + int left = 0; + if (mBlockStyle.mAlign.isHCenter()) + left += (pag.getPageWidth() - width) / 2; + else if (mBlockStyle.mAlign.isLeft()) + left = 0; + else if (mBlockStyle.mAlign.isRight()) + left += pag.getPageWidth() - width; + + mImageBox = parent->createWidget ("ImageBox", + MyGUI::IntCoord(left, pag.getCurrentTop(), width, mImageHeight), MyGUI::Align::Left | MyGUI::Align::Top, + parent->getName() + MyGUI::utility::toString(parent->getChildCount())); + + std::string image = Misc::ResourceHelpers::correctBookartPath(src, width, mImageHeight); + mImageBox->setImageTexture(image); + mImageBox->setProperty("NeedMouse", "false"); + } + + int ImageElement::getHeight() + { + return mImageHeight; } - } + int ImageElement::pageSplit() + { + // if the image is larger than the page, fall back to the default pageSplit implementation + if (mImageHeight > mPaginator.getPageHeight()) + return GraphicElement::pageSplit(); + return mPaginator.getCurrentTop(); + } + } } diff --git a/apps/openmw/mwgui/formatting.hpp b/apps/openmw/mwgui/formatting.hpp index a32d98fe5..d525baace 100644 --- a/apps/openmw/mwgui/formatting.hpp +++ b/apps/openmw/mwgui/formatting.hpp @@ -1,67 +1,173 @@ #ifndef MWGUI_FORMATTING_H #define MWGUI_FORMATTING_H -#include +#include +#include namespace MWGui { - struct TextStyle + namespace Formatting { - TextStyle() : - mColour(0,0,0) - , mFont("Default") - , mTextSize(16) - , mTextAlign(MyGUI::Align::Left | MyGUI::Align::Top) + struct TextStyle { - } + TextStyle() : + mColour(0,0,0) + , mFont("Default") + , mTextSize(16) + { + } - MyGUI::Colour mColour; - std::string mFont; - int mTextSize; - MyGUI::Align mTextAlign; - }; + MyGUI::Colour mColour; + std::string mFont; + int mTextSize; + }; - /// \brief utilities for parsing book/scroll text as mygui widgets - class BookTextParser - { - public: - /** - * Parse markup as MyGUI widgets - * @param markup to parse - * @param parent for the created widgets - * @param maximum width - * @return size of the created widgets - */ - MyGUI::IntSize parsePage(std::string text, MyGUI::Widget* parent, const int width); - - /** - * Parse markup as MyGUI widgets - * @param markup to parse - * @param parent for the created widgets - * @param maximum width - * @return size of the created widgets - */ - MyGUI::IntSize parseScroll(std::string text, MyGUI::Widget* parent, const int width); - - /** - * Split the specified text into pieces that fit in the area specified by width and height parameters - */ - std::vector split(std::string text, const int width, const int height); - - protected: - float widthForCharGlyph(unsigned unicodeChar) const; - float currentFontHeight() const; - void parseSubText(std::string text); - - void parseImage(std::string tag, bool createWidget=true); - void parseDiv(std::string tag); - void parseFont(std::string tag); - private: - MyGUI::Widget* mParent; - int mWidth; // maximum width - int mHeight; // current height - TextStyle mTextStyle; - }; + struct BlockStyle + { + BlockStyle() : + mAlign(MyGUI::Align::Left | MyGUI::Align::Top) + { + } + + MyGUI::Align mAlign; + }; + + class BookTextParser + { + public: + typedef std::map Attributes; + enum Events + { + Event_None = -2, + Event_EOF = -1, + Event_BrTag, + Event_PTag, + Event_ImgTag, + Event_DivTag, + Event_FontTag + }; + + BookTextParser(const std::string & text); + + Events next(); + + const Attributes & getAttributes() const; + std::string getReadyText() const; + bool isClosingTag() const; + + private: + void registerTag(const std::string & tag, Events type); + void flushBuffer(); + void parseTag(std::string tag); + + size_t mIndex; + std::string mText; + std::string mReadyText; + + bool mIgnoreNewlineTags; + bool mIgnoreLineEndings; + Attributes mAttributes; + std::string mTag; + bool mClosingTag; + std::map mTagTypes; + std::string mBuffer; + }; + + class Paginator + { + public: + typedef std::pair Page; + typedef std::vector Pages; + + Paginator(int pageWidth, int pageHeight) + : mStartTop(0), mCurrentTop(0), + mPageWidth(pageWidth), mPageHeight(pageHeight), + mIgnoreLeadingEmptyLines(false) + { + } + + int getStartTop() const { return mStartTop; } + int getCurrentTop() const { return mCurrentTop; } + int getPageWidth() const { return mPageWidth; } + int getPageHeight() const { return mPageHeight; } + bool getIgnoreLeadingEmptyLines() const { return mIgnoreLeadingEmptyLines; } + Pages getPages() const { return mPages; } + + void setStartTop(int top) { mStartTop = top; } + void setCurrentTop(int top) { mCurrentTop = top; } + void setIgnoreLeadingEmptyLines(bool ignore) { mIgnoreLeadingEmptyLines = ignore; } + + Paginator & operator<<(const Page & page) + { + mPages.push_back(page); + return *this; + } + + private: + int mStartTop, mCurrentTop; + int mPageWidth, mPageHeight; + bool mIgnoreLeadingEmptyLines; + Pages mPages; + }; + + /// \brief utilities for parsing book/scroll text as mygui widgets + class BookFormatter + { + public: + Paginator::Pages markupToWidget(MyGUI::Widget * parent, const std::string & markup, const int pageWidth, const int pageHeight); + Paginator::Pages markupToWidget(MyGUI::Widget * parent, const std::string & markup); + + private: + void resetFontProperties(); + + void handleDiv(const BookTextParser::Attributes & attr); + void handleFont(const BookTextParser::Attributes & attr); + + TextStyle mTextStyle; + BlockStyle mBlockStyle; + }; + + class GraphicElement + { + public: + GraphicElement(MyGUI::Widget * parent, Paginator & pag, const BlockStyle & blockStyle); + virtual int getHeight() = 0; + virtual void paginate(); + virtual int pageSplit(); + + protected: + virtual ~GraphicElement() {} + MyGUI::Widget * mParent; + Paginator & mPaginator; + BlockStyle mBlockStyle; + }; + + class TextElement : public GraphicElement + { + public: + TextElement(MyGUI::Widget * parent, Paginator & pag, const BlockStyle & blockStyle, + const TextStyle & textStyle, const std::string & text); + virtual int getHeight(); + virtual int pageSplit(); + private: + int currentFontHeight() const; + TextStyle mTextStyle; + MyGUI::EditBox * mEditBox; + }; + + class ImageElement : public GraphicElement + { + public: + ImageElement(MyGUI::Widget * parent, Paginator & pag, const BlockStyle & blockStyle, + const std::string & src, int width, int height); + virtual int getHeight(); + virtual int pageSplit(); + + private: + int mImageHeight; + MyGUI::ImageBox * mImageBox; + }; + } } #endif diff --git a/apps/openmw/mwgui/hud.cpp b/apps/openmw/mwgui/hud.cpp index 3e753b7f9..a4e67e9b1 100644 --- a/apps/openmw/mwgui/hud.cpp +++ b/apps/openmw/mwgui/hud.cpp @@ -1,12 +1,24 @@ #include "hud.hpp" -#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" #include "../mwworld/class.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/npcstats.hpp" @@ -15,7 +27,7 @@ #include "console.hpp" #include "spellicons.hpp" #include "itemmodel.hpp" -#include "container.hpp" +#include "draganddrop.hpp" #include "itemmodel.hpp" #include "itemwidget.hpp" @@ -59,8 +71,9 @@ namespace MWGui }; - HUD::HUD(int width, int height, int fpsLevel, DragAndDrop* dragAndDrop) + HUD::HUD(CustomMarkerCollection &customMarkers, int fpsLevel, DragAndDrop* dragAndDrop) : Layout("openmw_hud.layout") + , LocalMapBase(customMarkers) , mHealth(NULL) , mMagicka(NULL) , mStamina(NULL) @@ -97,7 +110,7 @@ namespace MWGui , mWeaponSpellTimer(0.f) , mDrowningFlashTheta(0.f) { - setCoord(0,0, width, height); + mMainWidget->setSize(MyGUI::RenderManager::getInstance().getViewSize()); // Energy bars getWidget(mHealthFrame, "HealthFrame"); @@ -159,7 +172,7 @@ namespace MWGui getWidget(mTriangleCounter, "TriangleCounter"); getWidget(mBatchCounter, "BatchCounter"); - LocalMapBase::init(mMinimap, mCompass, this); + LocalMapBase::init(mMinimap, mCompass, Settings::Manager::getInt("local map hud widget size", "Map")); mMainWidget->eventMouseButtonClick += MyGUI::newDelegate(this, &HUD::onWorldClicked); mMainWidget->eventMouseMove += MyGUI::newDelegate(this, &HUD::onWorldMouseOver); @@ -170,6 +183,10 @@ namespace MWGui HUD::~HUD() { + mMainWidget->eventMouseLostFocus.clear(); + mMainWidget->eventMouseMove.clear(); + mMainWidget->eventMouseButtonClick.clear(); + delete mSpellIcons; } @@ -200,17 +217,17 @@ namespace MWGui void HUD::setFPS(float fps) { if (mFpsCounter) - mFpsCounter->setCaption(boost::lexical_cast((int)fps)); + mFpsCounter->setCaption(MyGUI::utility::toString((int)fps)); } void HUD::setTriangleCount(unsigned int count) { - mTriangleCounter->setCaption(boost::lexical_cast(count)); + mTriangleCounter->setCaption(MyGUI::utility::toString(count)); } void HUD::setBatchCount(unsigned int count) { - mBatchCounter->setCaption(boost::lexical_cast(count)); + mBatchCounter->setCaption(MyGUI::utility::toString(count)); } void HUD::setValue(const std::string& id, const MWMechanics::DynamicStat& value) @@ -219,7 +236,7 @@ namespace MWGui int modified = static_cast(value.getModified()); MyGUI::Widget* w; - std::string valStr = boost::lexical_cast(current) + "/" + boost::lexical_cast(modified); + std::string valStr = MyGUI::utility::toString(current) + "/" + MyGUI::utility::toString(modified); if (id == "HBar") { mHealth->setProgressRange(modified); @@ -405,11 +422,6 @@ namespace MWGui mDrowningFlashTheta += dt * Ogre::Math::TWO_PI; } - void HUD::onResChange(int width, int height) - { - setCoord(0, 0, width, height); - } - void HUD::setSelectedSpell(const std::string& spellId, int successChancePercent) { const ESM::Spell* spell = @@ -435,10 +447,9 @@ namespace MWGui MWBase::Environment::get().getWorld()->getStore().get().find(spell->mEffects.mList.front().mEffectID); std::string icon = effect->mIcon; - int slashPos = icon.find("\\"); + int slashPos = icon.rfind('\\'); icon.insert(slashPos+1, "b_"); - icon = std::string("icons\\") + icon; - Widgets::fixTexturePath(icon); + icon = Misc::ResourceHelpers::correctIconPath(icon); mSpellImage->setItem(MWWorld::Ptr()); mSpellImage->setIcon(icon); @@ -475,6 +486,7 @@ namespace MWGui mWeaponSpellBox->setVisible(true); } + mWeapBox->clearUserStrings(); mWeapBox->setUserString("ToolTipType", "ItemPtr"); mWeapBox->setUserData(item); @@ -519,12 +531,14 @@ namespace MWGui MWWorld::Ptr player = world->getPlayerPtr(); mWeapImage->setItem(MWWorld::Ptr()); - if (player.getClass().getNpcStats(player).isWerewolf()) - mWeapImage->setIcon("icons\\k\\tx_werewolf_hand.dds"); - else - mWeapImage->setIcon("icons\\k\\stealth_handtohand.dds"); + std::string icon = (player.getClass().getNpcStats(player).isWerewolf()) ? "icons\\k\\tx_werewolf_hand.dds" : "icons\\k\\stealth_handtohand.dds"; + mWeapImage->setIcon(icon); mWeapBox->clearUserStrings(); + mWeapBox->setUserString("ToolTipType", "Layout"); + mWeapBox->setUserString("ToolTipLayout", "HandToHandToolTip"); + mWeapBox->setUserString("Caption_HandToHandText", itemName); + mWeapBox->setUserString("ImageTexture_HandToHandImage", icon); } void HUD::setCrosshairVisible(bool visible) @@ -618,6 +632,11 @@ namespace MWGui // Health is usually cast to int before displaying. Actors die whenever they are < 1 health. // Therefore any value < 1 should show as an empty health bar. We do the same in statswindow :) mEnemyHealth->setProgressPosition(int(stats.getHealth().getCurrent()) / stats.getHealth().getModified() * 100); + + static const float fNPCHealthBarFade = MWBase::Environment::get().getWorld()->getStore().get().find("fNPCHealthBarFade")->getFloat(); + if (fNPCHealthBarFade > 0.f) + mEnemyHealth->setAlpha(std::max(0.f, std::min(1.f, mEnemyHealthTimer/fNPCHealthBarFade))); + } void HUD::update() @@ -639,7 +658,7 @@ namespace MWGui void HUD::setEnemy(const MWWorld::Ptr &enemy) { mEnemyActorId = enemy.getClass().getCreatureStats(enemy).getActorId(); - mEnemyHealthTimer = 5; + mEnemyHealthTimer = MWBase::Environment::get().getWorld()->getStore().get().find("fNPCHealthBarTime")->getFloat(); if (!mEnemyHealth->getVisible()) mWeaponSpellBox->setPosition(mWeaponSpellBox->getPosition() - MyGUI::IntPoint(0,20)); mEnemyHealth->setVisible(true); @@ -652,4 +671,14 @@ namespace MWGui mEnemyHealthTimer = -1; } + void HUD::customMarkerCreated(MyGUI::Widget *marker) + { + marker->eventMouseButtonClick += MyGUI::newDelegate(this, &HUD::onMapClicked); + } + + void HUD::doorMarkerCreated(MyGUI::Widget *marker) + { + marker->eventMouseButtonClick += MyGUI::newDelegate(this, &HUD::onMapClicked); + } + } diff --git a/apps/openmw/mwgui/hud.hpp b/apps/openmw/mwgui/hud.hpp index 56b0ef7b5..263c08774 100644 --- a/apps/openmw/mwgui/hud.hpp +++ b/apps/openmw/mwgui/hud.hpp @@ -4,7 +4,11 @@ #include "mapwindow.hpp" #include "../mwmechanics/stat.hpp" -#include "../mwworld/ptr.hpp" + +namespace MWWorld +{ + class Ptr; +} namespace MWGui { @@ -15,7 +19,7 @@ namespace MWGui class HUD : public OEngine::GUI::Layout, public LocalMapBase { public: - HUD(int width, int height, int fpsLevel, DragAndDrop* dragAndDrop); + HUD(CustomMarkerCollection& customMarkers, int fpsLevel, DragAndDrop* dragAndDrop); virtual ~HUD(); void setValue (const std::string& id, const MWMechanics::DynamicStat& value); void setFPS(float fps); @@ -47,7 +51,6 @@ namespace MWGui void setCrosshairVisible(bool visible); void onFrame(float dt); - void onResChange(int width, int height); void setCellName(const std::string& cellName); @@ -75,8 +78,6 @@ namespace MWGui MyGUI::TextBox* mWeaponSpellBox; MyGUI::Widget *mDrowningFrame, *mDrowningFlash; - MyGUI::Widget* mDummy; - MyGUI::Widget* mFpsBox; MyGUI::TextBox* mFpsCounter; MyGUI::TextBox* mTriangleCounter; @@ -118,6 +119,10 @@ namespace MWGui void onMagicClicked(MyGUI::Widget* _sender); void onMapClicked(MyGUI::Widget* _sender); + // LocalMapBase + virtual void customMarkerCreated(MyGUI::Widget* marker); + virtual void doorMarkerCreated(MyGUI::Widget* marker); + void updateEnemyHealthBar(); void updatePositions(); diff --git a/apps/openmw/mwgui/inventoryitemmodel.cpp b/apps/openmw/mwgui/inventoryitemmodel.cpp index ad1a4e953..e8354b740 100644 --- a/apps/openmw/mwgui/inventoryitemmodel.cpp +++ b/apps/openmw/mwgui/inventoryitemmodel.cpp @@ -61,16 +61,11 @@ void InventoryItemModel::removeItem (const ItemStack& item, size_t count) MWWorld::Ptr InventoryItemModel::moveItem(const ItemStack &item, size_t count, ItemModel *otherModel) { - bool setNewOwner = false; + // Can't move conjured items: This is a general fix that also takes care of issues with taking conjured items via the 'Take All' button. + if (item.mFlags & ItemStack::Flag_Bound) + return MWWorld::Ptr(); - // Are you dead? Then you wont need that anymore - if (mActor.getClass().isActor() && mActor.getClass().getCreatureStats(mActor).isDead() - // Make sure that the item is actually owned by the dead actor - // Prevents a potential exploit for resetting the owner of any item, by placing the item in a corpse - && Misc::StringUtils::ciEqual(item.mBase.getCellRef().getOwner(), mActor.getCellRef().getRefId())) - setNewOwner = true; - - MWWorld::Ptr ret = otherModel->copyItem(item, count, setNewOwner); + MWWorld::Ptr ret = otherModel->copyItem(item, count, false); removeItem(item, count); return ret; } @@ -95,14 +90,8 @@ void InventoryItemModel::update() if (mActor.getClass().hasInventoryStore(mActor)) { MWWorld::InventoryStore& store = mActor.getClass().getInventoryStore(mActor); - for (int slot=0; slot -#include +#include +#include +#include +#include +#include + +#include #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" @@ -15,6 +21,7 @@ #include "../mwworld/action.hpp" #include "../mwscript/interpretercontext.hpp" #include "../mwbase/scriptmanager.hpp" +#include "../mwrender/characterpreview.hpp" #include "bookwindow.hpp" #include "scrollwindow.hpp" @@ -25,7 +32,21 @@ #include "tradeitemmodel.hpp" #include "countdialog.hpp" #include "tradewindow.hpp" -#include "container.hpp" +#include "draganddrop.hpp" +#include "widgets.hpp" + +namespace +{ + + bool isRightHandWeapon(const MWWorld::Ptr& item) + { + if (item.getClass().getTypeName() != typeid(ESM::Weapon).name()) + return false; + std::vector equipmentSlots = item.getClass().getEquipmentSlots(item).first; + return (!equipmentSlots.empty() && equipmentSlots.front() == MWWorld::InventoryStore::Slot_CarriedRight); + } + +} namespace MWGui { @@ -37,11 +58,14 @@ namespace MWGui , mLastYSize(0) , mPreview(new MWRender::InventoryPreview(MWBase::Environment::get().getWorld ()->getPlayerPtr())) , mPreviewDirty(true) + , mPreviewResize(true) , mDragAndDrop(dragAndDrop) + , mSortModel(NULL) + , mTradeModel(NULL) , mSelectedItem(-1) , mGuiMode(GM_Inventory) { - static_cast(mMainWidget)->eventWindowChangeCoord += MyGUI::newDelegate(this, &InventoryWindow::onWindowResize); + mMainWidget->castType()->eventWindowChangeCoord += MyGUI::newDelegate(this, &InventoryWindow::onWindowResize); getWidget(mAvatar, "Avatar"); getWidget(mAvatarImage, "AvatarImage"); @@ -91,8 +115,18 @@ namespace MWGui mTradeModel = new TradeItemModel(new InventoryItemModel(mPtr), MWWorld::Ptr()); mSortModel = new SortFilterItemModel(mTradeModel); mItemView->setModel(mSortModel); + + mPreview.reset(NULL); + mAvatarImage->setImageTexture(""); + MyGUI::ITexture* tex = MyGUI::RenderManager::getInstance().getTexture("CharacterPreview"); + if (tex) + MyGUI::RenderManager::getInstance().destroyTexture(tex); + mPreview.reset(new MWRender::InventoryPreview(mPtr)); mPreview->setup(); + + mPreviewDirty = true; + mPreviewResize = true; } void InventoryWindow::setGuiMode(GuiMode mode) @@ -125,7 +159,7 @@ namespace MWGui Settings::Manager::getFloat(setting + " h", "Windows") * viewSize.height); if (size.width != mMainWidget->getWidth() || size.height != mMainWidget->getHeight()) - mPreviewDirty = true; + mPreviewResize = true; mMainWidget->setPosition(pos); mMainWidget->setSize(size); @@ -172,21 +206,20 @@ namespace MWGui MWWorld::Ptr object = item.mBase; int count = item.mCount; - // Bound items may not be moved - if (item.mBase.getCellRef().getRefId().size() > 6 - && item.mBase.getCellRef().getRefId().substr(0,6) == "bound_") - { - MWBase::Environment::get().getSoundManager()->playSound (sound, 1.0, 1.0); - MWBase::Environment::get().getWindowManager()->messageBox("#{sBarterDialog12}"); - return; - } - bool shift = MyGUI::InputManager::getInstance().isShiftPressed(); if (MyGUI::InputManager::getInstance().isControlPressed()) count = 1; if (mTrading) { + // Can't give conjured items to a merchant + if (item.mFlags & ItemStack::Flag_Bound) + { + MWBase::Environment::get().getSoundManager()->playSound (sound, 1.0, 1.0); + MWBase::Environment::get().getWindowManager()->messageBox("#{sBarterDialog9}"); + return; + } + // check if merchant accepts item int services = MWBase::Environment::get().getWindowManager()->getTradeWindow()->getMerchantServices(); if (!object.getClass().canSell(object, services)) @@ -218,9 +251,6 @@ namespace MWGui else dragItem (NULL, count); } - - // item might have been unequipped - notifyContentChanged(); } void InventoryWindow::ensureSelectedItemUnequipped() @@ -258,6 +288,7 @@ namespace MWGui { ensureSelectedItemUnequipped(); mDragAndDrop->startDrag(mSelectedItem, mSortModel, mTradeModel, mItemView, count); + notifyContentChanged(); } void InventoryWindow::sellItem(MyGUI::Widget* sender, int count) @@ -270,21 +301,25 @@ namespace MWGui if (item.mType == ItemStack::Type_Barter) { // this was an item borrowed to us by the merchant - MWBase::Environment::get().getWindowManager()->getTradeWindow()->returnItem(mSelectedItem, count); mTradeModel->returnItemBorrowedToUs(mSelectedItem, count); + MWBase::Environment::get().getWindowManager()->getTradeWindow()->returnItem(mSelectedItem, count); } else { // borrow item to the merchant - MWBase::Environment::get().getWindowManager()->getTradeWindow()->borrowItem(mSelectedItem, count); mTradeModel->borrowItemFromUs(mSelectedItem, count); + MWBase::Environment::get().getWindowManager()->getTradeWindow()->borrowItem(mSelectedItem, count); } mItemView->update(); + notifyContentChanged(); } void InventoryWindow::updateItemView() { + if (MWBase::Environment::get().getWindowManager()->getSpellWindow()) + MWBase::Environment::get().getWindowManager()->getSpellWindow()->updateSpells(); + mItemView->update(); mPreviewDirty = true; } @@ -333,7 +368,7 @@ namespace MWGui { mLastXSize = mMainWidget->getSize().width; mLastYSize = mMainWidget->getSize().height; - mPreviewDirty = true; + mPreviewResize = true; } } @@ -358,7 +393,7 @@ namespace MWGui mItemView->update(); - static_cast(_sender)->setStateSelected(true); + _sender->castType()->setStateSelected(true); } void InventoryWindow::onPinToggled() @@ -366,6 +401,12 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->setWeaponVisibility(!mPinned); } + void InventoryWindow::onTitleDoubleClicked() + { + if (!mPinned) + MWBase::Environment::get().getWindowManager()->toggleVisible(GW_Inventory); + } + void InventoryWindow::useItem(const MWWorld::Ptr &ptr) { const std::string& script = ptr.getClass().getScript(ptr); @@ -379,7 +420,7 @@ namespace MWGui // Give the script a chance to run once before we do anything else // this is important when setting pcskipequip as a reaction to onpcequip being set (bk_treasuryreport does this) - if (!script.empty()) + if (!script.empty() && MWBase::Environment::get().getWorld()->getScriptsEnabled()) { MWScript::InterpreterContext interpreterContext (&ptr.getRefData().getLocals(), ptr); MWBase::Environment::get().getScriptManager()->run (script, interpreterContext); @@ -403,9 +444,13 @@ namespace MWGui else mSkippedToEquip = ptr; - mItemView->update(); + if (isVisible()) + { + mItemView->update(); - notifyContentChanged(); + notifyContentChanged(); + } + // else: will be updated in open() } void InventoryWindow::onAvatarClicked(MyGUI::Widget* _sender) @@ -473,6 +518,7 @@ namespace MWGui float capacity = player.getClass().getCapacity(player); float encumbrance = player.getClass().getEncumbrance(player); + mTradeModel->adjustEncumbrance(encumbrance); mEncumbranceBar->setValue(encumbrance, capacity); } @@ -491,21 +537,31 @@ namespace MWGui void InventoryWindow::doRenderUpdate () { - if (mPreviewDirty) + mPreview->onFrame(); + + if (mPreviewResize || mPreviewDirty) { - mPreviewDirty = false; + mArmorRating->setCaptionWithReplacing ("#{sArmor}: " + + MyGUI::utility::toString(static_cast(mPtr.getClass().getArmorRating(mPtr)))); + if (mArmorRating->getTextSize().width > mArmorRating->getSize().width) + mArmorRating->setCaptionWithReplacing (MyGUI::utility::toString(static_cast(mPtr.getClass().getArmorRating(mPtr)))); + } + if (mPreviewResize) + { + mPreviewResize = false; MyGUI::IntSize size = mAvatarImage->getSize(); - - mPreview->update (size.width, size.height); + mPreview->resize(size.width, size.height); mAvatarImage->setImageTexture("CharacterPreview"); mAvatarImage->setImageCoord(MyGUI::IntCoord(0, 0, std::min(512, size.width), std::min(1024, size.height))); mAvatarImage->setImageTile(MyGUI::IntSize(std::min(512, size.width), std::min(1024, size.height))); + } + if (mPreviewDirty) + { + mPreviewDirty = false; + mPreview->update (); - mArmorRating->setCaptionWithReplacing ("#{sArmor}: " - + boost::lexical_cast(static_cast(mPtr.getClass().getArmorRating(mPtr)))); - if (mArmorRating->getTextSize().width > mArmorRating->getSize().width) - mArmorRating->setCaptionWithReplacing (boost::lexical_cast(static_cast(mPtr.getClass().getArmorRating(mPtr)))); + mAvatarImage->setImageTexture("CharacterPreview"); } } @@ -515,6 +571,9 @@ namespace MWGui if (MWBase::Environment::get().getWindowManager()->getSpellWindow()) MWBase::Environment::get().getWindowManager()->getSpellWindow()->updateSpells(); + MWBase::Environment::get().getMechanicsManager()->updateMagicEffects( + MWBase::Environment::get().getWorld()->getPlayerPtr()); + mPreviewDirty = true; } @@ -565,6 +624,63 @@ namespace MWGui throw std::runtime_error("Added item not found"); mDragAndDrop->startDrag(i, mSortModel, mTradeModel, mItemView, count); - MWBase::Environment::get().getMechanicsManager()->itemTaken(player, newObject, count); + MWBase::Environment::get().getMechanicsManager()->itemTaken(player, newObject, MWWorld::Ptr(), count); + + if (MWBase::Environment::get().getWindowManager()->getSpellWindow()) + MWBase::Environment::get().getWindowManager()->getSpellWindow()->updateSpells(); + } + + void InventoryWindow::cycle(bool next) + { + ItemModel::ModelIndex selected = -1; + // not using mSortFilterModel as we only need sorting, not filtering + SortFilterItemModel model(new InventoryItemModel(MWBase::Environment::get().getWorld()->getPlayerPtr())); + model.setSortByType(false); + model.update(); + if (model.getItemCount() == 0) + return; + + for (ItemModel::ModelIndex i=0; irebuild(); } } diff --git a/apps/openmw/mwgui/inventorywindow.hpp b/apps/openmw/mwgui/inventorywindow.hpp index ae7af5719..ee71d9b04 100644 --- a/apps/openmw/mwgui/inventorywindow.hpp +++ b/apps/openmw/mwgui/inventorywindow.hpp @@ -1,14 +1,23 @@ #ifndef MGUI_Inventory_H #define MGUI_Inventory_H -#include "../mwrender/characterpreview.hpp" - #include "windowpinnablebase.hpp" -#include "widgets.hpp" #include "mode.hpp" +#include "../mwworld/ptr.hpp" + +namespace MWRender +{ + class InventoryPreview; +} + namespace MWGui { + namespace Widgets + { + class MWDynamicStat; + } + class ItemView; class SortFilterItemModel; class TradeItemModel; @@ -33,9 +42,7 @@ namespace MWGui MWWorld::Ptr getAvatarSelectedItem(int x, int y); - void rebuildAvatar() { - mPreview->rebuild(); - } + void rebuildAvatar(); SortFilterItemModel* getSortFilterModel(); TradeItemModel* getTradeModel(); @@ -49,10 +56,14 @@ namespace MWGui void setGuiMode(GuiMode mode); + /// Cycle to previous/next weapon + void cycle(bool next); + private: DragAndDrop* mDragAndDrop; bool mPreviewDirty; + bool mPreviewResize; int mSelectedItem; MWWorld::Ptr mPtr; @@ -98,6 +109,7 @@ namespace MWGui void onFilterChanged(MyGUI::Widget* _sender); void onAvatarClicked(MyGUI::Widget* _sender); void onPinToggled(); + void onTitleDoubleClicked(); void updateEncumbranceBar(); void notifyContentChanged(); diff --git a/apps/openmw/mwgui/itemmodel.cpp b/apps/openmw/mwgui/itemmodel.cpp index 59b54e171..8224fd55b 100644 --- a/apps/openmw/mwgui/itemmodel.cpp +++ b/apps/openmw/mwgui/itemmodel.cpp @@ -1,7 +1,14 @@ #include "itemmodel.hpp" +#include + #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" +#include "../mwworld/store.hpp" +#include "../mwworld/esmstore.hpp" + +#include "../mwbase/world.hpp" +#include "../mwbase/environment.hpp" namespace MWGui { @@ -15,6 +22,40 @@ namespace MWGui { if (base.getClass().getEnchantment(base) != "") mFlags |= Flag_Enchanted; + + static std::set boundItemIDCache; + + // If this is empty then we haven't executed the GMST cache logic yet; or there isn't any sMagicBound* GMST's for some reason + if (boundItemIDCache.empty()) + { + // Build a list of known bound item ID's + const MWWorld::Store &gameSettings = MWBase::Environment::get().getWorld()->getStore().get(); + + for (MWWorld::Store::iterator currentIteration = gameSettings.begin(); currentIteration != gameSettings.end(); ++currentIteration) + { + const ESM::GameSetting ¤tSetting = *currentIteration; + std::string currentGMSTID = currentSetting.mId; + Misc::StringUtils::toLower(currentGMSTID); + + // Don't bother checking this GMST if it's not a sMagicBound* one. + const std::string& toFind = "smagicbound"; + if (currentGMSTID.compare(0, toFind.length(), toFind) != 0) + continue; + + // All sMagicBound* GMST's should be of type string + std::string currentGMSTValue = currentSetting.getString(); + Misc::StringUtils::toLower(currentGMSTValue); + + boundItemIDCache.insert(currentGMSTValue); + } + } + + // Perform bound item check and assign the Flag_Bound bit if it passes + std::string tempItemID = base.getCellRef().getRefId(); + Misc::StringUtils::toLower(tempItemID); + + if (boundItemIDCache.count(tempItemID) != 0) + mFlags |= Flag_Bound; } ItemStack::ItemStack() diff --git a/apps/openmw/mwgui/itemmodel.hpp b/apps/openmw/mwgui/itemmodel.hpp index 21c5477d0..53c7018e2 100644 --- a/apps/openmw/mwgui/itemmodel.hpp +++ b/apps/openmw/mwgui/itemmodel.hpp @@ -26,7 +26,8 @@ namespace MWGui enum Flags { - Flag_Enchanted = (1<<0) + Flag_Enchanted = (1<<0), + Flag_Bound = (1<<1) }; int mFlags; @@ -45,20 +46,27 @@ namespace MWGui ItemModel(); virtual ~ItemModel() {} - typedef int ModelIndex; + typedef int ModelIndex; // -1 means invalid index + /// Throws for invalid index or out of range index virtual ItemStack getItem (ModelIndex index) = 0; - ///< throws for invalid index + + /// The number of items in the model, this specifies the range of indices you can pass to + /// the getItem function (but this range is only valid until the next call to update()) virtual size_t getItemCount() = 0; + /// Returns an invalid index if the item was not found virtual ModelIndex getIndex (ItemStack item) = 0; + /// Rebuild the item model, this will invalidate existing model indices virtual void update() = 0; /// Move items from this model to \a otherModel. + /// @note Derived implementations may return an empty Ptr if the move was unsuccessful. virtual MWWorld::Ptr moveItem (const ItemStack& item, size_t count, ItemModel* otherModel); - /// @param setNewOwner Set the copied item's owner to the actor we are copying to, or keep the original owner? + /// @param setNewOwner If true, set the copied item's owner to the actor we are copying to, + /// otherwise reset owner to "" virtual MWWorld::Ptr copyItem (const ItemStack& item, size_t count, bool setNewOwner=false) = 0; virtual void removeItem (const ItemStack& item, size_t count) = 0; diff --git a/apps/openmw/mwgui/itemselection.cpp b/apps/openmw/mwgui/itemselection.cpp index 1b197f6d8..916f13360 100644 --- a/apps/openmw/mwgui/itemselection.cpp +++ b/apps/openmw/mwgui/itemselection.cpp @@ -1,5 +1,8 @@ #include "itemselection.hpp" +#include +#include + #include "itemview.hpp" #include "inventoryitemmodel.hpp" #include "sortfilteritemmodel.hpp" diff --git a/apps/openmw/mwgui/itemselection.hpp b/apps/openmw/mwgui/itemselection.hpp index 28c45c605..50e15a3fe 100644 --- a/apps/openmw/mwgui/itemselection.hpp +++ b/apps/openmw/mwgui/itemselection.hpp @@ -1,7 +1,14 @@ #ifndef OPENMW_GAME_MWGUI_ITEMSELECTION_H #define OPENMW_GAME_MWGUI_ITEMSELECTION_H -#include "container.hpp" +#include + +#include "windowbase.hpp" + +namespace MWWorld +{ + class Ptr; +} namespace MWGui { diff --git a/apps/openmw/mwgui/itemview.cpp b/apps/openmw/mwgui/itemview.cpp index b86f61034..e30f6e7a8 100644 --- a/apps/openmw/mwgui/itemview.cpp +++ b/apps/openmw/mwgui/itemview.cpp @@ -1,6 +1,6 @@ #include "itemview.hpp" -#include +#include #include #include @@ -17,16 +17,6 @@ namespace MWGui { -std::string ItemView::getCountString(int count) -{ - if (count == 1) - return ""; - if (count > 9999) - return boost::lexical_cast(int(count/1000.f)) + "k"; - else - return boost::lexical_cast(count); -} - ItemView::ItemView() : mModel(NULL) , mScrollView(NULL) @@ -56,6 +46,49 @@ void ItemView::initialiseOverride() mScrollView->setCanvasAlign(MyGUI::Align::Left | MyGUI::Align::Top); } +void ItemView::layoutWidgets() +{ + if (!mScrollView->getChildCount()) + return; + + int x = 0; + int y = 0; + MyGUI::Widget* dragArea = mScrollView->getChildAt(0); + int maxHeight = mScrollView->getHeight(); + + int rows = maxHeight/42; + rows = std::max(rows, 1); + bool showScrollbar = int(std::ceil(dragArea->getChildCount()/float(rows))) > mScrollView->getWidth()/42; + if (showScrollbar) + maxHeight -= 18; + + for (unsigned int i=0; igetChildCount(); ++i) + { + MyGUI::Widget* w = dragArea->getChildAt(i); + + w->setPosition(x, y); + + y += 42; + + if (y > maxHeight-42 && i < dragArea->getChildCount()-1) + { + x += 42; + y = 0; + } + } + x += 42; + + MyGUI::IntSize size = MyGUI::IntSize(std::max(mScrollView->getSize().width, x), mScrollView->getSize().height); + + // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden + mScrollView->setVisibleVScroll(false); + mScrollView->setVisibleHScroll(false); + mScrollView->setCanvasSize(size); + mScrollView->setVisibleVScroll(true); + mScrollView->setVisibleHScroll(true); + dragArea->setSize(size); +} + void ItemView::update() { while (mScrollView->getChildCount()) @@ -64,10 +97,6 @@ void ItemView::update() if (!mModel) return; - int x = 0; - int y = 0; - int maxHeight = mScrollView->getSize().height - 58; - mModel->update(); MyGUI::Widget* dragArea = mScrollView->createWidget("",0,0,mScrollView->getWidth(),mScrollView->getHeight(), @@ -80,9 +109,8 @@ void ItemView::update() { const ItemStack& item = mModel->getItem(i); - /// \todo performance improvement: don't create/destroy all the widgets everytime the container window changes size, only reposition them ItemWidget* itemWidget = dragArea->createWidget("MW_ItemIcon", - MyGUI::IntCoord(x, y, 42, 42), MyGUI::Align::Default); + MyGUI::IntCoord(0, 0, 42, 42), MyGUI::Align::Default); itemWidget->setUserString("ToolTipType", "ItemModelIndex"); itemWidget->setUserData(std::make_pair(i, mModel)); ItemWidget::ItemState state = ItemWidget::None; @@ -91,32 +119,13 @@ void ItemView::update() if (item.mType == ItemStack::Type_Equipped) state = ItemWidget::Equip; itemWidget->setItem(item.mBase, state); + itemWidget->setCount(item.mCount); itemWidget->eventMouseButtonClick += MyGUI::newDelegate(this, &ItemView::onSelectedItem); itemWidget->eventMouseWheel += MyGUI::newDelegate(this, &ItemView::onMouseWheel); - - // text widget that shows item count - // TODO: move to ItemWidget - MyGUI::TextBox* text = itemWidget->createWidget("SandBrightText", - MyGUI::IntCoord(5, 19, 32, 18), MyGUI::Align::Default, std::string("Label")); - text->setTextAlign(MyGUI::Align::Right); - text->setNeedMouseFocus(false); - text->setTextShadow(true); - text->setTextShadowColour(MyGUI::Colour(0,0,0)); - text->setCaption(getCountString(item.mCount)); - - y += 42; - if (y > maxHeight) - { - x += 42; - y = 0; - } - } - x += 42; - MyGUI::IntSize size = MyGUI::IntSize(std::max(mScrollView->getSize().width, x), mScrollView->getSize().height); - mScrollView->setCanvasSize(size); - dragArea->setSize(size); + + layoutWidgets(); } void ItemView::onSelectedItem(MyGUI::Widget *sender) @@ -143,12 +152,7 @@ void ItemView::setSize(const MyGUI::IntSize &_value) bool changed = (_value.width != getWidth() || _value.height != getHeight()); Base::setSize(_value); if (changed) - update(); -} - -void ItemView::setSize(int _width, int _height) -{ - setSize(MyGUI::IntSize(_width, _height)); + layoutWidgets(); } void ItemView::setCoord(const MyGUI::IntCoord &_value) @@ -156,12 +160,7 @@ void ItemView::setCoord(const MyGUI::IntCoord &_value) bool changed = (_value.width != getWidth() || _value.height != getHeight()); Base::setCoord(_value); if (changed) - update(); -} - -void ItemView::setCoord(int _left, int _top, int _width, int _height) -{ - setCoord(MyGUI::IntCoord(_left, _top, _width, _height)); + layoutWidgets(); } void ItemView::registerComponents() diff --git a/apps/openmw/mwgui/itemview.hpp b/apps/openmw/mwgui/itemview.hpp index 74bc66ea0..9aeba6752 100644 --- a/apps/openmw/mwgui/itemview.hpp +++ b/apps/openmw/mwgui/itemview.hpp @@ -30,15 +30,13 @@ namespace MWGui void update(); - static std::string getCountString(int count); - private: virtual void initialiseOverride(); + void layoutWidgets(); + virtual void setSize(const MyGUI::IntSize& _value); virtual void setCoord(const MyGUI::IntCoord& _value); - void setSize(int _width, int _height); - void setCoord(int _left, int _top, int _width, int _height); void onSelectedItem (MyGUI::Widget* sender); void onSelectedBackground (MyGUI::Widget* sender); diff --git a/apps/openmw/mwgui/itemwidget.cpp b/apps/openmw/mwgui/itemwidget.cpp index 7c79f8027..36940113e 100644 --- a/apps/openmw/mwgui/itemwidget.cpp +++ b/apps/openmw/mwgui/itemwidget.cpp @@ -2,14 +2,32 @@ #include #include +#include + +#include #include "../mwworld/class.hpp" +namespace +{ + std::string getCountString(int count) + { + if (count == 1) + return ""; + if (count > 9999) + return MyGUI::utility::toString(int(count/1000.f)) + "k"; + else + return MyGUI::utility::toString(count); + } +} + namespace MWGui { ItemWidget::ItemWidget() : mItem(NULL) + , mFrame(NULL) + , mText(NULL) { } @@ -27,32 +45,28 @@ namespace MWGui assignWidget(mFrame, "Frame"); if (mFrame) mFrame->setNeedMouseFocus(false); + assignWidget(mText, "Text"); + if (mText) + mText->setNeedMouseFocus(false); Base::initialiseOverride(); } - void ItemWidget::setIcon(const std::string &icon) + void ItemWidget::setCount(int count) { - // HACK HACK HACK: Don't setImageTexture if it hasn't changed. - // There is a leak in MyGUI for each setImageTexture on the same widget. - // http://www.ogre3d.org/addonforums/viewtopic.php?f=17&t=30251 - if (mCurrentItemTexture == icon) + if (!mText) return; + mText->setCaption(getCountString(count)); + } - mCurrentItemTexture = icon; + void ItemWidget::setIcon(const std::string &icon) + { if (mItem) mItem->setImageTexture(icon); } void ItemWidget::setFrame(const std::string &frame, const MyGUI::IntCoord &coord) { - // HACK HACK HACK: Don't setImageTexture if it hasn't changed. - // There is a leak in MyGUI for each setImageTexture on the same widget. - // http://www.ogre3d.org/addonforums/viewtopic.php?f=17&t=30251 - if (mCurrentFrameTexture == frame) - return; - - mCurrentFrameTexture = frame; if (mFrame) { mFrame->setImageTexture(frame); @@ -63,15 +77,7 @@ namespace MWGui void ItemWidget::setIcon(const MWWorld::Ptr &ptr) { - // image - std::string path = std::string("icons\\"); - path += ptr.getClass().getInventoryIcon(ptr); - - std::string::size_type pos = path.rfind("."); - if(pos != std::string::npos) - path.erase(pos); - path.append(".dds"); - setIcon(path); + setIcon(Misc::ResourceHelpers::correctIconPath(ptr.getClass().getInventoryIcon(ptr))); } @@ -83,21 +89,9 @@ namespace MWGui if (ptr.isEmpty()) { if (mFrame) - { - // HACK HACK HACK: Don't setImageTexture if it hasn't changed. - // There is a leak in MyGUI for each setImageTexture on the same widget. - // http://www.ogre3d.org/addonforums/viewtopic.php?f=17&t=30251 - if (!mCurrentFrameTexture.empty()) - { - mFrame->setImageTexture(""); - mCurrentFrameTexture = ""; - } - } - if (!mCurrentItemTexture.empty()) - { - mCurrentItemTexture = ""; - mItem->setImageTexture(""); - } + mFrame->setImageTexture(""); + mItem->setImageTexture(""); + mText->setCaption(""); return; } diff --git a/apps/openmw/mwgui/itemwidget.hpp b/apps/openmw/mwgui/itemwidget.hpp index 5cdf71212..e7a902239 100644 --- a/apps/openmw/mwgui/itemwidget.hpp +++ b/apps/openmw/mwgui/itemwidget.hpp @@ -29,6 +29,9 @@ namespace MWGui Magic }; + /// Set count to be displayed in a textbox over the item + void setCount(int count); + /// \a ptr may be empty void setItem (const MWWorld::Ptr& ptr, ItemState state = None); @@ -42,9 +45,7 @@ namespace MWGui MyGUI::ImageBox* mItem; MyGUI::ImageBox* mFrame; - - std::string mCurrentItemTexture; - std::string mCurrentFrameTexture; + MyGUI::TextBox* mText; }; } diff --git a/apps/openmw/mwgui/jailscreen.cpp b/apps/openmw/mwgui/jailscreen.cpp new file mode 100644 index 000000000..58873c566 --- /dev/null +++ b/apps/openmw/mwgui/jailscreen.cpp @@ -0,0 +1,129 @@ +#include + +#include "../mwbase/windowmanager.hpp" +#include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/world.hpp" +#include "../mwbase/environment.hpp" + +#include "../mwmechanics/npcstats.hpp" + +#include "../mwworld/esmstore.hpp" +#include "../mwworld/store.hpp" +#include "../mwworld/class.hpp" + +#include "jailscreen.hpp" + +namespace MWGui +{ + JailScreen::JailScreen() + : WindowBase("openmw_jail_screen.layout"), + mTimeAdvancer(0.01), + mDays(1), + mFadeTimeRemaining(0) + { + getWidget(mProgressBar, "ProgressBar"); + + setVisible(false); + + mTimeAdvancer.eventProgressChanged += MyGUI::newDelegate(this, &JailScreen::onJailProgressChanged); + mTimeAdvancer.eventFinished += MyGUI::newDelegate(this, &JailScreen::onJailFinished); + + center(); + } + + void JailScreen::goToJail(int days) + { + mDays = days; + + MWBase::Environment::get().getWindowManager()->fadeScreenOut(0.5); + mFadeTimeRemaining = 0.5; + + setVisible(false); + mProgressBar->setScrollRange(100+1); + mProgressBar->setScrollPosition(0); + mProgressBar->setTrackSize(0); + } + + void JailScreen::onFrame(float dt) + { + mTimeAdvancer.onFrame(dt); + + if (mFadeTimeRemaining <= 0) + return; + + mFadeTimeRemaining -= dt; + + if (mFadeTimeRemaining <= 0) + { + MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); + MWBase::Environment::get().getWorld()->teleportToClosestMarker(player, "prisonmarker"); + + setVisible(true); + mTimeAdvancer.run(100); + } + } + + void JailScreen::onJailProgressChanged(int cur, int /*total*/) + { + mProgressBar->setScrollPosition(0); + mProgressBar->setTrackSize(cur / (float)(mProgressBar->getScrollRange()) * mProgressBar->getLineSize()); + } + + void JailScreen::onJailFinished() + { + MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_Jail); + MWBase::Environment::get().getWindowManager()->fadeScreenIn(0.5); + + MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); + + MWBase::Environment::get().getWorld()->advanceTime(mDays * 24); + for (int i=0; irest(true); + + std::set skills; + for (int day=0; day (RAND_MAX) + 1) * ESM::Skill::Length; + skills.insert(skill); + + MWMechanics::SkillValue& value = player.getClass().getNpcStats(player).getSkill(skill); + if (skill == ESM::Skill::Security || skill == ESM::Skill::Sneak) + value.setBase(std::min(100, value.getBase()+1)); + else + value.setBase(value.getBase()-1); + } + + const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); + + std::string message; + if (mDays == 1) + message = gmst.find("sNotifyMessage42")->getString(); + else + message = gmst.find("sNotifyMessage43")->getString(); + + std::stringstream dayStr; + dayStr << mDays; + if (message.find("%d") != std::string::npos) + message.replace(message.find("%d"), 2, dayStr.str()); + + for (std::set::iterator it = skills.begin(); it != skills.end(); ++it) + { + std::string skillName = gmst.find(ESM::Skill::sSkillNameIds[*it])->getString(); + std::stringstream skillValue; + skillValue << player.getClass().getNpcStats(player).getSkill(*it).getBase(); + std::string skillMsg = gmst.find("sNotifyMessage44")->getString(); + if (*it == ESM::Skill::Sneak || *it == ESM::Skill::Security) + skillMsg = gmst.find("sNotifyMessage39")->getString(); + + if (skillMsg.find("%s") != std::string::npos) + skillMsg.replace(skillMsg.find("%s"), 2, skillName); + if (skillMsg.find("%d") != std::string::npos) + skillMsg.replace(skillMsg.find("%d"), 2, skillValue.str()); + message += "\n" + skillMsg; + } + + std::vector buttons; + buttons.push_back("#{sOk}"); + MWBase::Environment::get().getWindowManager()->interactiveMessageBox(message, buttons); + } +} diff --git a/apps/openmw/mwgui/jailscreen.hpp b/apps/openmw/mwgui/jailscreen.hpp new file mode 100644 index 000000000..7a544126d --- /dev/null +++ b/apps/openmw/mwgui/jailscreen.hpp @@ -0,0 +1,31 @@ +#ifndef MWGUI_JAILSCREEN_H +#define MWGUI_JAILSCREEN_H + +#include "windowbase.hpp" +#include "timeadvancer.hpp" + +namespace MWGui +{ + class JailScreen : public WindowBase + { + public: + JailScreen(); + void goToJail(int days); + + void onFrame(float dt); + + private: + int mDays; + + float mFadeTimeRemaining; + + MyGUI::ScrollBar* mProgressBar; + + void onJailProgressChanged(int cur, int total); + void onJailFinished(); + + TimeAdvancer mTimeAdvancer; + }; +} + +#endif diff --git a/apps/openmw/mwgui/journalbooks.cpp b/apps/openmw/mwgui/journalbooks.cpp index 7ffe9e6a4..34a852562 100644 --- a/apps/openmw/mwgui/journalbooks.cpp +++ b/apps/openmw/mwgui/journalbooks.cpp @@ -1,10 +1,13 @@ #include "journalbooks.hpp" +#include + namespace { - const MyGUI::Colour linkHot (0.40f, 0.40f, 0.80f); - const MyGUI::Colour linkNormal (0.20f, 0.20f, 0.60f); - const MyGUI::Colour linkActive (0.50f, 0.50f, 1.00f); + MyGUI::Colour getTextColour (const std::string& type) + { + return MyGUI::Colour::parse(MyGUI::LanguageManager::getInstance().replaceTags("#{fontcolour=" + type + "}")); + } struct AddContent { @@ -28,6 +31,10 @@ namespace { MWGui::BookTypesetter::Style* style = mBodyStyle; + static const MyGUI::Colour linkHot (getTextColour("journal_link_over")); + static const MyGUI::Colour linkNormal (getTextColour("journal_link")); + static const MyGUI::Colour linkActive (getTextColour("journal_link_pressed")); + if (topicId) style = mTypesetter->createHotStyle (mBodyStyle, linkNormal, linkHot, linkActive, topicId); @@ -132,22 +139,6 @@ namespace mTypesetter->sectionBreak (10); } }; - - struct AddTopicLink : AddContent - { - AddTopicLink (MWGui::BookTypesetter::Ptr typesetter, MWGui::BookTypesetter::Style* style) : - AddContent (typesetter, style) - { - } - - void operator () (MWGui::JournalViewModel::TopicId topicId, MWGui::JournalViewModel::Utf8Span name) - { - MWGui::BookTypesetter::Style* link = mTypesetter->createHotStyle (mBodyStyle, MyGUI::Colour::Black, linkHot, linkActive, topicId); - - mTypesetter->write (link, name); - mTypesetter->lineBreak (); - } - }; } namespace MWGui @@ -242,7 +233,11 @@ book JournalBooks::createTopicIndexBook () sprintf (buffer, "( %c )", ch); - BookTypesetter::Style* style = typesetter->createHotStyle (body, MyGUI::Colour::Black, linkHot, linkActive, ch); + MyGUI::Colour linkHot (getTextColour("journal_topic_over")); + MyGUI::Colour linkActive (getTextColour("journal_topic_pressed")); + MyGUI::Colour linkNormal (getTextColour("journal_topic")); + + BookTypesetter::Style* style = typesetter->createHotStyle (body, linkNormal, linkHot, linkActive, ch); if (i == 13) typesetter->sectionBreak (); diff --git a/apps/openmw/mwgui/journalviewmodel.cpp b/apps/openmw/mwgui/journalviewmodel.cpp index b71e64a19..9a47070c2 100644 --- a/apps/openmw/mwgui/journalviewmodel.cpp +++ b/apps/openmw/mwgui/journalviewmodel.cpp @@ -8,13 +8,15 @@ #include +#include + #include "../mwbase/world.hpp" #include "../mwbase/journal.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" -#include "../mwdialogue/journalentry.hpp" -#include "keywordsearch.hpp" +#include "../mwdialogue/journalentry.hpp" +#include "../mwdialogue/keywordsearch.hpp" namespace MWGui { @@ -22,7 +24,7 @@ struct JournalViewModelImpl; struct JournalViewModelImpl : JournalViewModel { - typedef KeywordSearch KeywordSearchT; + typedef MWDialogue::KeywordSearch KeywordSearchT; mutable bool mKeywordSearchLoaded; mutable KeywordSearchT mKeywordSearch; @@ -113,10 +115,10 @@ struct JournalViewModelImpl : JournalViewModel utf8text = getText (); - size_t pos_begin, pos_end; + size_t pos_end = 0; for(;;) { - pos_begin = utf8text.find('@'); + size_t pos_begin = utf8text.find('@'); if (pos_begin != std::string::npos) pos_end = utf8text.find('#', pos_begin); @@ -174,12 +176,14 @@ struct JournalViewModelImpl : JournalViewModel } else { - std::string::const_iterator i = utf8text.begin (); - - KeywordSearchT::Match match; + std::vector matches; + mModel->mKeywordSearch.highlightKeywords(utf8text.begin(), utf8text.end(), matches); - while (i != utf8text.end () && mModel->mKeywordSearch.search (i, utf8text.end (), match, utf8text.begin())) + std::string::const_iterator i = utf8text.begin (); + for (std::vector::const_iterator it = matches.begin(); it != matches.end(); ++it) { + const KeywordSearchT::Match& match = *it; + if (i != match.mBeg) visitor (0, i - utf8text.begin (), match.mBeg - utf8text.begin ()); @@ -195,18 +199,29 @@ struct JournalViewModelImpl : JournalViewModel }; - void visitQuestNames (bool active_only, boost::function visitor) const + void visitQuestNames (bool active_only, boost::function visitor) const { MWBase::Journal * journal = MWBase::Environment::get ().getJournal (); std::set visitedQuests; + // Note that for purposes of the journal GUI, quests are identified by the name, not the ID, so several + // different quest IDs can end up in the same quest log. A quest log should be considered finished + // when any quest ID in that log is finished. for (MWBase::Journal::TQuestIter i = journal->questBegin (); i != journal->questEnd (); ++i) { - if (active_only && i->second.isFinished ()) + const MWDialogue::Quest& quest = i->second; + + bool isFinished = false; + for (MWBase::Journal::TQuestIter j = journal->questBegin (); j != journal->questEnd (); ++j) + { + if (quest.getName() == j->second.getName() && j->second.isFinished()) + isFinished = true; + } + + if (active_only && isFinished) continue; - const MWDialogue::Quest& quest = i->second; // Unfortunately Morrowind.esm has no quest names, since the quest book was added with tribunal. // Note that even with Tribunal, some quests still don't have quest names. I'm assuming those are not supposed // to appear in the quest book. @@ -216,22 +231,13 @@ struct JournalViewModelImpl : JournalViewModel if (visitedQuests.find(quest.getName()) != visitedQuests.end()) continue; - visitor (quest.getName()); + visitor (quest.getName(), isFinished); visitedQuests.insert(quest.getName()); } } } - void visitQuestName (QuestId questId, boost::function visitor) const - { - MWDialogue::Quest const * quest = reinterpret_cast (questId); - - std::string name = quest->getName (); - - visitor (toUtf8Span (name)); - } - template struct JournalEntryImpl : BaseEntry { @@ -301,11 +307,6 @@ struct JournalViewModelImpl : JournalViewModel } } - void visitTopics (boost::function visitor) const - { - throw std::runtime_error ("not implemented"); - } - void visitTopicName (TopicId topicId, boost::function visitor) const { MWDialogue::Topic const & topic = * reinterpret_cast (topicId); diff --git a/apps/openmw/mwgui/journalviewmodel.hpp b/apps/openmw/mwgui/journalviewmodel.hpp index 4b827d1ab..b3c6b0183 100644 --- a/apps/openmw/mwgui/journalviewmodel.hpp +++ b/apps/openmw/mwgui/journalviewmodel.hpp @@ -67,11 +67,8 @@ namespace MWGui /// returns true if their are no journal entries to display virtual bool isEmpty () const = 0; - /// provides access to the name of the quest with the specified identifier - virtual void visitQuestName (TopicId topicId, boost::function visitor) const = 0; - - /// walks the active and optionally completed, quests providing the name - virtual void visitQuestNames (bool active_only, boost::function visitor) const = 0; + /// walks the active and optionally completed, quests providing the name and completed status + virtual void visitQuestNames (bool active_only, boost::function visitor) const = 0; /// walks over the journal entries related to all quests with the given name /// If \a questName is empty, simply visits all journal entries diff --git a/apps/openmw/mwgui/journalwindow.cpp b/apps/openmw/mwgui/journalwindow.cpp index fa27b4ef0..4cfcc8064 100644 --- a/apps/openmw/mwgui/journalwindow.cpp +++ b/apps/openmw/mwgui/journalwindow.cpp @@ -1,26 +1,30 @@ #include "journalwindow.hpp" -#include "../mwbase/environment.hpp" -#include "../mwbase/soundmanager.hpp" -#include "../mwbase/windowmanager.hpp" -#include "../mwbase/journal.hpp" -#include "list.hpp" - #include #include #include #include #include + +#include +#include + #include #include -#include "boost/lexical_cast.hpp" + +#include +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/soundmanager.hpp" +#include "../mwbase/windowmanager.hpp" +#include "../mwbase/journal.hpp" #include "bookpage.hpp" #include "windowbase.hpp" -#include "imagebutton.hpp" #include "journalviewmodel.hpp" #include "journalbooks.hpp" -#include "list.hpp" namespace { @@ -71,7 +75,7 @@ namespace void setText (char const * name, value_type const & value) { getWidget (name) -> - setCaption (boost::lexical_cast (value)); + setCaption (MyGUI::utility::toString (value)); } void setVisible (char const * name, bool visible) @@ -82,7 +86,7 @@ namespace void adviseButtonClick (char const * name, void (JournalWindowImpl::*Handler) (MyGUI::Widget* _sender)) { - getWidget (name) -> + getWidget (name) -> eventMouseButtonClick += newDelegate(this, Handler); } @@ -110,10 +114,10 @@ namespace adviseButtonClick (ShowAllBTN, &JournalWindowImpl::notifyShowAll ); adviseButtonClick (ShowActiveBTN, &JournalWindowImpl::notifyShowActive); - MWGui::Widgets::MWList* list = getWidget(QuestsList); + Gui::MWList* list = getWidget(QuestsList); list->eventItemSelected += MyGUI::newDelegate(this, &JournalWindowImpl::notifyQuestClicked); - MWGui::Widgets::MWList* topicsList = getWidget(TopicsList); + Gui::MWList* topicsList = getWidget(TopicsList); topicsList->eventItemSelected += MyGUI::newDelegate(this, &JournalWindowImpl::notifyTopicSelected); { @@ -123,6 +127,9 @@ namespace getPage (LeftBookPage)->adviseLinkClicked (callback); getPage (RightBookPage)->adviseLinkClicked (callback); + + getPage (LeftBookPage)->eventMouseWheel += MyGUI::newDelegate(this, &JournalWindowImpl::notifyMouseWheel); + getPage (RightBookPage)->eventMouseWheel += MyGUI::newDelegate(this, &JournalWindowImpl::notifyMouseWheel); } { @@ -143,12 +150,12 @@ namespace adjustButton(ShowActiveBTN, true); adjustButton(JournalBTN); - MWGui::ImageButton* optionsButton = getWidget(OptionsBTN); + Gui::ImageButton* optionsButton = getWidget(OptionsBTN); if (optionsButton->getWidth() == 0) { // If tribunal is not installed (-> no options button), we still want the Topics button available, // so place it where the options button would have been - MWGui::ImageButton* topicsButton = getWidget(TopicsBTN); + Gui::ImageButton* topicsButton = getWidget(TopicsBTN); topicsButton->detachFromWidget(); topicsButton->attachToWidget(optionsButton->getParent()); topicsButton->setPosition(optionsButton->getPosition()); @@ -156,7 +163,7 @@ namespace topicsButton->eventMouseButtonClick += MyGUI::newDelegate(this, &JournalWindowImpl::notifyOptions); } - MWGui::ImageButton* nextButton = getWidget(NextPageBTN); + Gui::ImageButton* nextButton = getWidget(NextPageBTN); if (nextButton->getSize().width == 64) { // english button has a 7 pixel wide strip of garbage on its right edge @@ -179,7 +186,7 @@ namespace void adjustButton (char const * name, bool optional = false) { - MWGui::ImageButton* button = getWidget(name); + Gui::ImageButton* button = getWidget(name); MyGUI::IntSize diff = button->getSize() - button->getRequestedSize(!optional); button->setSize(button->getRequestedSize(!optional)); @@ -198,10 +205,6 @@ namespace setBookMode (); - /// \todo Wiping the whole book layout each time the journal is opened is probably too costly for a large journal (eg 300+ pages). - /// There should be a way to keep the existing layout and append new entries to the end of it. - /// However, that still leaves the problem of having to add links to previously unknown, but now known topics, so - /// we maybe need to find another way to speed things up. Book journalBook; if (mModel->isEmpty ()) journalBook = createEmptyJournalBook (); @@ -394,22 +397,13 @@ namespace popBook (); } - void showList (char const * listId, char const * pageId, Book book) - { - std::pair size = book->getSize (); - - getPage (pageId)->showPage (book, 0); - - getWidget (listId)->setCanvasSize (size.first, size.second); - } - void notifyIndexLinkClicked (MWGui::TypesetBook::InteractiveId character) { setVisible (LeftTopicIndex, false); setVisible (RightTopicIndex, false); setVisible (TopicsList, true); - MWGui::Widgets::MWList* list = getWidget(TopicsList); + Gui::MWList* list = getWidget(TopicsList); list->clear(); AddNamesToList add(list); @@ -432,14 +426,27 @@ namespace struct AddNamesToList { - AddNamesToList(MWGui::Widgets::MWList* list) : mList(list) {} + AddNamesToList(Gui::MWList* list) : mList(list) {} - MWGui::Widgets::MWList* mList; - void operator () (const std::string& name) + Gui::MWList* mList; + void operator () (const std::string& name, bool finished=false) { mList->addItem(name); } }; + struct SetNamesInactive + { + SetNamesInactive(Gui::MWList* list) : mList(list) {} + + Gui::MWList* mList; + void operator () (const std::string& name, bool finished) + { + if (finished) + { + mList->getItemWidget(name)->setStateSelected(true); + } + } + }; void notifyQuests(MyGUI::Widget* _sender) { @@ -452,7 +459,7 @@ namespace setVisible (ShowAllBTN, !mAllQuests); setVisible (ShowActiveBTN, mAllQuests); - MWGui::Widgets::MWList* list = getWidget(QuestsList); + Gui::MWList* list = getWidget(QuestsList); list->clear(); AddNamesToList add(list); @@ -460,6 +467,12 @@ namespace mModel->visitQuestNames(!mAllQuests, add); list->adjustSize(); + + if (mAllQuests) + { + SetNamesInactive setInactive(list); + mModel->visitQuestNames(!mAllQuests, setInactive); + } } void notifyShowAll(MyGUI::Widget* _sender) @@ -485,6 +498,14 @@ namespace MWBase::Environment::get().getWindowManager ()->popGuiMode (); } + void notifyMouseWheel(MyGUI::Widget* sender, int rel) + { + if (rel < 0) + notifyNextPage(sender); + else + notifyPrevPage(sender); + } + void notifyNextPage(MyGUI::Widget* _sender) { if (!mStates.empty ()) @@ -492,7 +513,7 @@ namespace unsigned int & page = mStates.top ().mPage; Book book = mStates.top ().mBook; - if (page < book->pageCount () - 2) + if (page+2 < book->pageCount()) { page += 2; updateShowingPages (); @@ -506,7 +527,7 @@ namespace { unsigned int & page = mStates.top ().mPage; - if(page > 0) + if(page >= 2) { page -= 2; updateShowingPages (); diff --git a/apps/openmw/mwgui/journalwindow.hpp b/apps/openmw/mwgui/journalwindow.hpp index 63770ec1a..5d2a5318a 100644 --- a/apps/openmw/mwgui/journalwindow.hpp +++ b/apps/openmw/mwgui/journalwindow.hpp @@ -1,7 +1,6 @@ #ifndef MWGUI_JOURNAL_H #define MWGUI_JOURNAL_H -#include #include namespace MWBase { class WindowManager; } diff --git a/apps/openmw/mwgui/levelupdialog.cpp b/apps/openmw/mwgui/levelupdialog.cpp index 32aaa6e71..c392372ff 100644 --- a/apps/openmw/mwgui/levelupdialog.cpp +++ b/apps/openmw/mwgui/levelupdialog.cpp @@ -1,10 +1,13 @@ #include "levelupdialog.hpp" -#include +#include +#include +#include #include "../mwbase/windowmanager.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" +#include "../mwbase/soundmanager.hpp" #include "../mwworld/class.hpp" #include "../mwworld/fallback.hpp" @@ -26,23 +29,24 @@ namespace MWGui getWidget(mLevelText, "LevelText"); getWidget(mLevelDescription, "LevelDescription"); getWidget(mCoinBox, "Coins"); + getWidget(mAssignWidget, "AssignWidget"); mOkButton->eventMouseButtonClick += MyGUI::newDelegate(this, &LevelupDialog::onOkButtonClicked); for (int i=1; i<9; ++i) { MyGUI::TextBox* t; - getWidget(t, "AttribVal" + boost::lexical_cast(i)); + getWidget(t, "AttribVal" + MyGUI::utility::toString(i)); MyGUI::Button* b; - getWidget(b, "Attrib" + boost::lexical_cast(i)); + getWidget(b, "Attrib" + MyGUI::utility::toString(i)); b->setUserData (i-1); b->eventMouseButtonClick += MyGUI::newDelegate(this, &LevelupDialog::onAttributeClicked); mAttributes.push_back(b); mAttributeValues.push_back(t); - getWidget(t, "AttribMultiplier" + boost::lexical_cast(i)); + getWidget(t, "AttribMultiplier" + MyGUI::utility::toString(i)); mAttributeMultipliers.push_back(t); } @@ -74,7 +78,7 @@ namespace MWGui if (val >= 100) val = 100; - mAttributeValues[i]->setCaption(boost::lexical_cast(val)); + mAttributeValues[i]->setCaption(MyGUI::utility::toString(val)); } } @@ -83,13 +87,19 @@ namespace MWGui { const int coinSpacing = 10; int curX = mCoinBox->getWidth()/2 - (coinSpacing*(mCoinCount - 1) + 16*mCoinCount)/2; - for (unsigned int i=0; idetachFromWidget(); image->attachToWidget(mCoinBox); - image->setCoord(MyGUI::IntCoord(curX,0,16,16)); - curX += 16+coinSpacing; + if (i < mCoinCount) + { + mCoins[i]->setVisible(true); + image->setCoord(MyGUI::IntCoord(curX,0,16,16)); + curX += 16+coinSpacing; + } + else + mCoins[i]->setVisible(false); } } @@ -100,13 +110,13 @@ namespace MWGui { MyGUI::ImageBox* image = mCoins[i]; image->detachFromWidget(); - image->attachToWidget(mMainWidget); + image->attachToWidget(mAssignWidget); int attribute = mSpentAttributes[i]; int xdiff = mAttributeMultipliers[attribute]->getCaption() == "" ? 0 : 20; - MyGUI::IntPoint pos = mAttributes[attribute]->getAbsolutePosition() - mMainWidget->getAbsolutePosition() - MyGUI::IntPoint(22+xdiff,0); + MyGUI::IntPoint pos = mAttributes[attribute]->getAbsolutePosition() - mAssignWidget->getAbsolutePosition() - MyGUI::IntPoint(22+xdiff,0); pos.top += (mAttributes[attribute]->getHeight() - image->getHeight())/2; image->setPosition(pos); } @@ -129,10 +139,9 @@ namespace MWGui if(world->getStore().get().isDynamic(cls->mId)) { - // Vanilla uses thief.dds for custom classes. // Choosing Stealth specialization and Speed/Agility as attributes, if possible. Otherwise fall back to first class found. MWWorld::SharedIterator it = world->getStore().get().begin(); - for(; it != world->getStore().get().end(); it++) + for(; it != world->getStore().get().end(); ++it) { if(it->mData.mIsPlayable && it->mData.mSpecialization == 2 && it->mData.mAttribute[0] == 4 && it->mData.mAttribute[1] == 3) break; @@ -146,13 +155,13 @@ namespace MWGui mClassImage->setImageTexture ("textures\\levelup\\" + cls->mId + ".dds"); int level = creatureStats.getLevel ()+1; - mLevelText->setCaptionWithReplacing("#{sLevelUpMenu1} " + boost::lexical_cast(level)); + mLevelText->setCaptionWithReplacing("#{sLevelUpMenu1} " + MyGUI::utility::toString(level)); std::string levelupdescription; if(level > 20) levelupdescription=world->getFallback()->getFallbackString("Level_Up_Default"); else - levelupdescription=world->getFallback()->getFallbackString("Level_Up_Level"+boost::lexical_cast(level)); + levelupdescription=world->getFallback()->getFallbackString("Level_Up_Level"+MyGUI::utility::toString(level)); mLevelDescription->setCaption (levelupdescription); @@ -163,14 +172,17 @@ namespace MWGui if (pcStats.getAttribute(i).getBase() < 100) { mAttributes[i]->setEnabled(true); + mAttributeValues[i]->setEnabled(true); availableAttributes++; int mult = pcStats.getLevelupAttributeMultiplier (i); - text->setCaption(mult <= 1 ? "" : "x" + boost::lexical_cast(mult)); + mult = std::min(mult, 100-pcStats.getAttribute(i).getBase()); + text->setCaption(mult <= 1 ? "" : "x" + MyGUI::utility::toString(mult)); } else { mAttributes[i]->setEnabled(false); + mAttributeValues[i]->setEnabled(false); text->setCaption(""); } @@ -178,20 +190,15 @@ namespace MWGui mCoinCount = std::min(sMaxCoins, availableAttributes); - for (unsigned int i = 0; i < sMaxCoins; i++) - { - if (i < mCoinCount) - mCoins[i]->attachToWidget(mCoinBox); - else - mCoins[i]->detachFromWidget(); - } - mSpentAttributes.clear(); resetCoins(); setAttributeValues(); center(); + + // Play LevelUp Music + MWBase::Environment::get().getSoundManager()->streamMusic("Special/MW_Triumph.mp3"); } void LevelupDialog::onOkButtonClicked(MyGUI::Widget* sender) diff --git a/apps/openmw/mwgui/levelupdialog.hpp b/apps/openmw/mwgui/levelupdialog.hpp index 5ca9c8c75..e5edbd53c 100644 --- a/apps/openmw/mwgui/levelupdialog.hpp +++ b/apps/openmw/mwgui/levelupdialog.hpp @@ -20,6 +20,7 @@ namespace MWGui MyGUI::EditBox* mLevelDescription; MyGUI::Widget* mCoinBox; + MyGUI::Widget* mAssignWidget; std::vector mAttributes; std::vector mAttributeValues; diff --git a/apps/openmw/mwgui/list.cpp b/apps/openmw/mwgui/list.cpp deleted file mode 100644 index b0c514b9d..000000000 --- a/apps/openmw/mwgui/list.cpp +++ /dev/null @@ -1,165 +0,0 @@ -#include "list.hpp" - -#include -#include -#include -#include - -namespace MWGui -{ - - namespace Widgets - { - - MWList::MWList() : - mClient(0) - , mScrollView(0) - , mItemHeight(0) - { - } - - void MWList::initialiseOverride() - { - Base::initialiseOverride(); - - assignWidget(mClient, "Client"); - if (mClient == 0) - mClient = this; - - mScrollView = mClient->createWidgetReal( - "MW_ScrollView", MyGUI::FloatCoord(0.0, 0.0, 1.0, 1.0), - MyGUI::Align::Top | MyGUI::Align::Left | MyGUI::Align::Stretch, getName() + "_ScrollView"); - } - - void MWList::addItem(const std::string& name) - { - mItems.push_back(name); - } - - void MWList::addSeparator() - { - mItems.push_back(""); - } - - void MWList::adjustSize() - { - redraw(); - } - - void MWList::redraw(bool scrollbarShown) - { - const int _scrollBarWidth = 20; // fetch this from skin? - const int scrollBarWidth = scrollbarShown ? _scrollBarWidth : 0; - const int spacing = 3; - size_t viewPosition = -mScrollView->getViewOffset().top; - - while (mScrollView->getChildCount()) - { - MyGUI::Gui::getInstance().destroyWidget(mScrollView->getChildAt(0)); - } - - mItemHeight = 0; - int i=0; - for (std::vector::const_iterator it=mItems.begin(); - it!=mItems.end(); ++it) - { - if (*it != "") - { - if (mListItemSkin.empty()) - throw std::runtime_error("MWList needs a ListItemSkin property"); - MyGUI::Button* button = mScrollView->createWidget( - mListItemSkin, MyGUI::IntCoord(0, mItemHeight, mScrollView->getSize().width - scrollBarWidth - 2, 24), - MyGUI::Align::Left | MyGUI::Align::Top, getName() + "_item_" + (*it)); - button->setCaption((*it)); - button->getSubWidgetText()->setWordWrap(true); - button->getSubWidgetText()->setTextAlign(MyGUI::Align::Left); - button->eventMouseWheel += MyGUI::newDelegate(this, &MWList::onMouseWheel); - button->eventMouseButtonClick += MyGUI::newDelegate(this, &MWList::onItemSelected); - - int height = button->getTextSize().height; - button->setSize(MyGUI::IntSize(button->getSize().width, height)); - button->setUserData(i); - - mItemHeight += height + spacing; - } - else - { - MyGUI::ImageBox* separator = mScrollView->createWidget("MW_HLine", - MyGUI::IntCoord(2, mItemHeight, mScrollView->getWidth() - scrollBarWidth - 4, 18), - MyGUI::Align::Left | MyGUI::Align::Top | MyGUI::Align::HStretch); - separator->setNeedMouseFocus(false); - - mItemHeight += 18 + spacing; - } - ++i; - } - mScrollView->setCanvasSize(mClient->getSize().width, std::max(mItemHeight, mClient->getSize().height)); - - if (!scrollbarShown && mItemHeight > mClient->getSize().height) - redraw(true); - - size_t viewRange = mScrollView->getCanvasSize().height; - if(viewPosition > viewRange) - viewPosition = viewRange; - mScrollView->setViewOffset(MyGUI::IntPoint(0, viewPosition * -1)); - } - - void MWList::setPropertyOverride(const std::string &_key, const std::string &_value) - { - if (_key == "ListItemSkin") - mListItemSkin = _value; - else - Base::setPropertyOverride(_key, _value); - } - - bool MWList::hasItem(const std::string& name) - { - return (std::find(mItems.begin(), mItems.end(), name) != mItems.end()); - } - - unsigned int MWList::getItemCount() - { - return mItems.size(); - } - - std::string MWList::getItemNameAt(unsigned int at) - { - assert(at < mItems.size() && "List item out of bounds"); - return mItems[at]; - } - - void MWList::removeItem(const std::string& name) - { - assert( std::find(mItems.begin(), mItems.end(), name) != mItems.end() ); - mItems.erase( std::find(mItems.begin(), mItems.end(), name) ); - } - - void MWList::clear() - { - mItems.clear(); - } - - void MWList::onMouseWheel(MyGUI::Widget* _sender, int _rel) - { - //NB view offset is negative - if (mScrollView->getViewOffset().top + _rel*0.3 > 0) - mScrollView->setViewOffset(MyGUI::IntPoint(0, 0)); - else - mScrollView->setViewOffset(MyGUI::IntPoint(0, mScrollView->getViewOffset().top + _rel*0.3)); - } - - void MWList::onItemSelected(MyGUI::Widget* _sender) - { - std::string name = static_cast(_sender)->getCaption(); - int id = *_sender->getUserData(); - eventItemSelected(name, id); - eventWidgetSelected(_sender); - } - - MyGUI::Widget* MWList::getItemWidget(const std::string& name) - { - return mScrollView->findWidget (getName() + "_item_" + name); - } - - } -} diff --git a/apps/openmw/mwgui/list.hpp b/apps/openmw/mwgui/list.hpp deleted file mode 100644 index acf078a2c..000000000 --- a/apps/openmw/mwgui/list.hpp +++ /dev/null @@ -1,74 +0,0 @@ -#ifndef MWGUI_LIST_HPP -#define MWGUI_LIST_HPP - -#include - -namespace MWGui -{ - namespace Widgets - { - /** - * \brief a very simple list widget that supports word-wrapping entries - * \note if the width or height of the list changes, you must call adjustSize() method - */ - class MWList : public MyGUI::Widget - { - MYGUI_RTTI_DERIVED(MWList) - public: - MWList(); - - typedef MyGUI::delegates::CMultiDelegate2 EventHandle_StringInt; - typedef MyGUI::delegates::CMultiDelegate1 EventHandle_Widget; - - /** - * Event: Item selected with the mouse. - * signature: void method(std::string itemName, int index) - */ - EventHandle_StringInt eventItemSelected; - - /** - * Event: Item selected with the mouse. - * signature: void method(MyGUI::Widget* sender) - */ - EventHandle_Widget eventWidgetSelected; - - - /** - * Call after the size of the list changed, or items were inserted/removed - */ - void adjustSize(); - - void addItem(const std::string& name); - void addSeparator(); ///< add a seperator between the current and the next item. - void removeItem(const std::string& name); - bool hasItem(const std::string& name); - unsigned int getItemCount(); - std::string getItemNameAt(unsigned int at); ///< \attention if there are separators, this method will return "" at the place where the separator is - void clear(); - - MyGUI::Widget* getItemWidget(const std::string& name); - ///< get widget for an item name, useful to set up tooltip - - virtual void setPropertyOverride(const std::string& _key, const std::string& _value); - - protected: - void initialiseOverride(); - - void redraw(bool scrollbarShown = false); - - void onMouseWheel(MyGUI::Widget* _sender, int _rel); - void onItemSelected(MyGUI::Widget* _sender); - - private: - MyGUI::ScrollView* mScrollView; - MyGUI::Widget* mClient; - std::string mListItemSkin; - - std::vector mItems; - - int mItemHeight; // height of all items - }; - } -} - -#endif diff --git a/apps/openmw/mwgui/loadingscreen.cpp b/apps/openmw/mwgui/loadingscreen.cpp index f6d6b8135..9e3343c78 100644 --- a/apps/openmw/mwgui/loadingscreen.cpp +++ b/apps/openmw/mwgui/loadingscreen.cpp @@ -8,6 +8,14 @@ #include #include #include +#include + +#include +#include +#include +#include + +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -30,8 +38,11 @@ namespace MWGui , mProgress(0) , mVSyncWasEnabled(false) { + mMainWidget->setSize(MyGUI::RenderManager::getInstance().getViewSize()); + getWidget(mLoadingText, "LoadingText"); getWidget(mProgressBar, "ProgressBar"); + getWidget(mLoadingBox, "LoadingBox"); mProgressBar->setScrollViewPage(1); @@ -44,6 +55,11 @@ namespace MWGui void LoadingScreen::setLabel(const std::string &label) { mLoadingText->setCaptionWithReplacing(label); + int padding = mLoadingBox->getWidth() - mLoadingText->getWidth(); + MyGUI::IntSize size(mLoadingText->getTextSize().width+padding, mLoadingBox->getHeight()); + size.width = std::max(300, size.width); + mLoadingBox->setSize(size); + mLoadingBox->setPosition(mMainWidget->getWidth()/2 - mLoadingBox->getWidth()/2, mLoadingBox->getTop()); } LoadingScreen::~LoadingScreen() @@ -56,13 +72,6 @@ namespace MWGui mBackgroundImage->setVisible(visible); } - void LoadingScreen::onResChange(int w, int h) - { - setCoord(0,0,w,h); - - mBackgroundImage->setCoord(MyGUI::IntCoord(0,0,w,h)); - } - void LoadingScreen::loadingOn() { // Early-out if already on @@ -71,11 +80,8 @@ namespace MWGui // Temporarily turn off VSync, we want to do actual loading rather than waiting for the screen to sync. // Threaded loading would be even better, of course - especially because some drivers force VSync to on and we can't change it. - // In Ogre 1.8, the swapBuffers argument is useless and setVSyncEnabled is bugged with GLX, nothing we can do :/ mVSyncWasEnabled = mWindow->isVSyncEnabled(); - #if OGRE_VERSION >= (1 << 16 | 9 << 8 | 0) mWindow->setVSyncEnabled(false); - #endif bool showWallpaper = (MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_NoGame); @@ -118,10 +124,7 @@ namespace MWGui void LoadingScreen::loadingOff() { // Re-enable vsync now. - // In Ogre 1.8, the swapBuffers argument is useless and setVSyncEnabled is bugged with GLX, nothing we can do :/ - #if OGRE_VERSION >= (1 << 16 | 9 << 8 | 0) mWindow->setVSyncEnabled(mVSyncWasEnabled); - #endif setVisible(false); @@ -148,7 +151,9 @@ namespace MWGui Ogre::TextureManager::getSingleton ().load (randomSplash, Ogre::ResourceGroupManager::AUTODETECT_RESOURCE_GROUP_NAME); // TODO: add option (filename pattern?) to use image aspect ratio instead of 4:3 - mBackgroundImage->setBackgroundImage(randomSplash, true, true); + // we can't do this by default, because the Morrowind splash screens are 1024x1024, but should be displayed as 4:3 + bool stretch = Settings::Manager::getBool("stretch menu background", "GUI"); + mBackgroundImage->setBackgroundImage(randomSplash, true, stretch); } else std::cerr << "No loading screens found!" << std::endl; diff --git a/apps/openmw/mwgui/loadingscreen.hpp b/apps/openmw/mwgui/loadingscreen.hpp index f198d625d..0d3ffbbec 100644 --- a/apps/openmw/mwgui/loadingscreen.hpp +++ b/apps/openmw/mwgui/loadingscreen.hpp @@ -1,13 +1,18 @@ #ifndef MWGUI_LOADINGSCREEN_H #define MWGUI_LOADINGSCREEN_H -#include #include +#include #include "windowbase.hpp" #include +namespace Ogre +{ + class SceneManager; +} + namespace MWGui { class BackgroundImage; @@ -35,8 +40,6 @@ namespace MWGui void setLoadingProgress (const std::string& stage, int depth, int current, int total); void loadingDone(); - void onResChange(int w, int h); - void updateWindow(Ogre::RenderWindow* rw) { mWindow = rw; } private: @@ -49,6 +52,8 @@ namespace MWGui size_t mProgress; + MyGUI::Widget* mLoadingBox; + MyGUI::TextBox* mLoadingText; MyGUI::ScrollBar* mProgressBar; BackgroundImage* mBackgroundImage; diff --git a/apps/openmw/mwgui/mainmenu.cpp b/apps/openmw/mwgui/mainmenu.cpp index ca7e877cc..6ad4da3bf 100644 --- a/apps/openmw/mwgui/mainmenu.cpp +++ b/apps/openmw/mwgui/mainmenu.cpp @@ -2,8 +2,15 @@ #include +#include +#include +#include + #include +#include +#include + #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/soundmanager.hpp" @@ -16,7 +23,6 @@ #include "savegamedialog.hpp" #include "confirmationdialog.hpp" -#include "imagebutton.hpp" #include "backgroundimage.hpp" #include "videowidget.hpp" @@ -28,6 +34,7 @@ namespace MWGui , mButtonBox(0), mWidth (w), mHeight (h) , mSaveGameDialog(NULL) , mBackground(NULL) + , mVideoBackground(NULL) , mVideo(NULL) { getWidget(mVersionText, "VersionText"); @@ -42,7 +49,7 @@ namespace MWGui rev = rev.substr(0,10); sstream << "\nRevision: " << rev; } - + std::string output = sstream.str(); mVersionText->setCaption(output); @@ -92,7 +99,6 @@ namespace MWGui MWBase::Environment::get().getSoundManager()->playSound("Menu Click", 1.f, 1.f); if (name == "return") { - MWBase::Environment::get().getSoundManager ()->resumeSounds (MWBase::SoundManager::Play_TypeSfx); MWBase::Environment::get().getWindowManager ()->removeGuiMode (GM_MainMenu); } else if (name == "options") @@ -155,6 +161,8 @@ namespace MWGui if (!show) return; + bool stretch = Settings::Manager::getBool("stretch menu background", "GUI"); + if (mHasAnimatedMenu) { if (!mVideo) @@ -175,13 +183,7 @@ namespace MWGui int screenHeight = viewSize.height; mVideoBackground->setSize(screenWidth, screenHeight); - double imageaspect = static_cast(mVideo->getVideoWidth())/mVideo->getVideoHeight(); - - int leftPadding = std::max(0.0, (screenWidth - screenHeight * imageaspect) / 2); - int topPadding = std::max(0.0, (screenHeight - screenWidth / imageaspect) / 2); - - mVideo->setCoord(leftPadding, topPadding, - screenWidth - leftPadding*2, screenHeight - topPadding*2); + mVideo->autoResize(stretch); mVideo->setVisible(true); } @@ -191,7 +193,7 @@ namespace MWGui { mBackground = MyGUI::Gui::getInstance().createWidgetReal("ImageBox", 0,0,1,1, MyGUI::Align::Stretch, "Menu"); - mBackground->setBackgroundImage("textures\\menu_morrowind.dds"); + mBackground->setBackgroundImage("textures\\menu_morrowind.dds", true, stretch); } mBackground->setVisible(true); } @@ -250,7 +252,7 @@ namespace MWGui { if (mButtons.find(*it) == mButtons.end()) { - MWGui::ImageButton* button = mButtonBox->createWidget + Gui::ImageButton* button = mButtonBox->createWidget ("ImageBox", MyGUI::IntCoord(0, curH, 0, 0), MyGUI::Align::Default); button->setProperty("ImageHighlighted", "textures\\menu_" + *it + "_over.dds"); button->setProperty("ImageNormal", "textures\\menu_" + *it + ".dds"); @@ -263,7 +265,7 @@ namespace MWGui // Start by hiding all buttons int maxwidth = 0; - for (std::map::iterator it = mButtons.begin(); it != mButtons.end(); ++it) + for (std::map::iterator it = mButtons.begin(); it != mButtons.end(); ++it) { it->second->setVisible(false); MyGUI::IntSize requested = it->second->getRequestedSize(); @@ -275,7 +277,7 @@ namespace MWGui for (std::vector::iterator it = buttons.begin(); it != buttons.end(); ++it) { assert(mButtons.find(*it) != mButtons.end()); - MWGui::ImageButton* button = mButtons[*it]; + Gui::ImageButton* button = mButtons[*it]; button->setVisible(true); MyGUI::IntSize requested = button->getRequestedSize(); diff --git a/apps/openmw/mwgui/mainmenu.hpp b/apps/openmw/mwgui/mainmenu.hpp index ccd8df4b0..cd2050d0f 100644 --- a/apps/openmw/mwgui/mainmenu.hpp +++ b/apps/openmw/mwgui/mainmenu.hpp @@ -3,10 +3,14 @@ #include +namespace Gui +{ + class ImageButton; +} + namespace MWGui { - class ImageButton; class BackgroundImage; class SaveGameDialog; class VideoWidget; @@ -39,7 +43,7 @@ namespace MWGui MyGUI::ImageBox* mVideoBackground; VideoWidget* mVideo; // For animated main menus - std::map mButtons; + std::map mButtons; void onButtonClicked (MyGUI::Widget* sender); void onNewGameConfirmed(); diff --git a/apps/openmw/mwgui/mapwindow.cpp b/apps/openmw/mwgui/mapwindow.cpp index af26456f2..13e2e3904 100644 --- a/apps/openmw/mwgui/mapwindow.cpp +++ b/apps/openmw/mwgui/mapwindow.cpp @@ -1,82 +1,195 @@ #include "mapwindow.hpp" -#include - #include #include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwworld/player.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwrender/globalmap.hpp" -#include "../components/esm/globalmap.hpp" - #include "widgets.hpp" +#include "confirmationdialog.hpp" +#include "tooltips.hpp" + +namespace +{ + + const int cellSize = 8192; + + enum LocalMapWidgetDepth + { + Local_CompassLayer = 0, + Local_MarkerAboveFogLayer = 1, + Local_FogLayer = 2, + Local_MarkerLayer = 3, + Local_MapLayer = 4 + }; + + enum GlobalMapWidgetDepth + { + Global_CompassLayer = 0, + Global_MarkerLayer = 1, + Global_ExploreOverlayLayer = 2, + Global_MapLayer = 3 + }; + + + /// @brief A widget that changes its color when hovered. + class MarkerWidget: public MyGUI::Widget + { + MYGUI_RTTI_DERIVED(MarkerWidget) + + public: + void setNormalColour(const MyGUI::Colour& colour) + { + mNormalColour = colour; + setColour(colour); + } + + void setHoverColour(const MyGUI::Colour& colour) + { + mHoverColour = colour; + } + + private: + MyGUI::Colour mNormalColour; + MyGUI::Colour mHoverColour; + + void onMouseLostFocus(MyGUI::Widget* _new) + { + setColour(mNormalColour); + } + + void onMouseSetFocus(MyGUI::Widget* _old) + { + setColour(mHoverColour); + } + }; +} namespace MWGui { - LocalMapBase::LocalMapBase() + void CustomMarkerCollection::addMarker(const ESM::CustomMarker &marker, bool triggerEvent) + { + mMarkers.push_back(marker); + if (triggerEvent) + eventMarkersChanged(); + } + + void CustomMarkerCollection::deleteMarker(const ESM::CustomMarker &marker) + { + std::vector::iterator it = std::find(mMarkers.begin(), mMarkers.end(), marker); + if (it != mMarkers.end()) + mMarkers.erase(it); + else + throw std::runtime_error("can't find marker to delete"); + + eventMarkersChanged(); + } + + void CustomMarkerCollection::updateMarker(const ESM::CustomMarker &marker, const std::string &newNote) + { + std::vector::iterator it = std::find(mMarkers.begin(), mMarkers.end(), marker); + if (it != mMarkers.end()) + it->mNote = newNote; + else + throw std::runtime_error("can't find marker to update"); + + eventMarkersChanged(); + } + + void CustomMarkerCollection::clear() + { + mMarkers.clear(); + eventMarkersChanged(); + } + + std::vector::const_iterator CustomMarkerCollection::begin() const + { + return mMarkers.begin(); + } + + std::vector::const_iterator CustomMarkerCollection::end() const + { + return mMarkers.end(); + } + + size_t CustomMarkerCollection::size() const + { + return mMarkers.size(); + } + + // ------------------------------------------------------ + + LocalMapBase::LocalMapBase(CustomMarkerCollection &markers) : mCurX(0) , mCurY(0) , mInterior(false) , mFogOfWar(true) , mLocalMap(NULL) - , mMapDragAndDrop(false) , mPrefix() , mChanged(true) - , mLayout(NULL) - , mLastPositionX(0.0f) - , mLastPositionY(0.0f) , mLastDirectionX(0.0f) , mLastDirectionY(0.0f) , mCompass(NULL) , mMarkerUpdateTimer(0.0f) + , mCustomMarkers(markers) + , mMapWidgetSize(0) { + mCustomMarkers.eventMarkersChanged += MyGUI::newDelegate(this, &LocalMapBase::updateCustomMarkers); } LocalMapBase::~LocalMapBase() { - // Clear our "lost focus" delegate for marker widgets first, otherwise it will - // fire when the widget is about to be destroyed and the mouse cursor is over it. - // At that point, other widgets may already be destroyed, so applyFogOfWar (which is called by the delegate) would crash. - for (std::vector::iterator it = mDoorMarkerWidgets.begin(); it != mDoorMarkerWidgets.end(); ++it) - (*it)->eventMouseLostFocus.clear(); - for (std::vector::iterator it = mMarkerWidgets.begin(); it != mMarkerWidgets.end(); ++it) - (*it)->eventMouseLostFocus.clear(); + mCustomMarkers.eventMarkersChanged -= MyGUI::newDelegate(this, &LocalMapBase::updateCustomMarkers); } - void LocalMapBase::init(MyGUI::ScrollView* widget, MyGUI::ImageBox* compass, OEngine::GUI::Layout* layout, bool mapDragAndDrop) + void LocalMapBase::init(MyGUI::ScrollView* widget, MyGUI::ImageBox* compass, int mapWidgetSize) { mLocalMap = widget; - mLayout = layout; - mMapDragAndDrop = mapDragAndDrop; mCompass = compass; + mMapWidgetSize = mapWidgetSize; + + mLocalMap->setCanvasSize(mMapWidgetSize*3, mMapWidgetSize*3); + + mCompass->setDepth(Local_CompassLayer); + mCompass->setNeedMouseFocus(false); - // create 3x3 map widgets, 512x512 each, holding a 1024x1024 texture each - const int widgetSize = 512; for (int mx=0; mx<3; ++mx) { for (int my=0; my<3; ++my) { MyGUI::ImageBox* map = mLocalMap->createWidget("ImageBox", - MyGUI::IntCoord(mx*widgetSize, my*widgetSize, widgetSize, widgetSize), + MyGUI::IntCoord(mx*mMapWidgetSize, my*mMapWidgetSize, mMapWidgetSize, mMapWidgetSize), MyGUI::Align::Top | MyGUI::Align::Left); + map->setDepth(Local_MapLayer); - MyGUI::ImageBox* fog = map->createWidget("ImageBox", - MyGUI::IntCoord(0, 0, widgetSize, widgetSize), + MyGUI::ImageBox* fog = mLocalMap->createWidget("ImageBox", + MyGUI::IntCoord(mx*mMapWidgetSize, my*mMapWidgetSize, mMapWidgetSize, mMapWidgetSize), MyGUI::Align::Top | MyGUI::Align::Left); + fog->setDepth(Local_FogLayer); - if (!mMapDragAndDrop) - { - map->setNeedMouseFocus(false); - fog->setNeedMouseFocus(false); - } + map->setNeedMouseFocus(false); + fog->setNeedMouseFocus(false); mMapWidgets.push_back(map); mFogWidgets.push_back(fog); @@ -103,8 +216,8 @@ namespace MWGui { for (int my=0; my<3; ++my) { - std::string image = mPrefix+"_"+ boost::lexical_cast(mCurX + (mx-1)) + "_" - + boost::lexical_cast(mCurY + (-1*(my-1))); + std::string image = mPrefix+"_"+ MyGUI::utility::toString(mCurX + (mx-1)) + "_" + + MyGUI::utility::toString(mCurY + (-1*(my-1))); MyGUI::ImageBox* fog = mFogWidgets[my + 3*mx]; fog->setImageTexture(mFogOfWar ? ((MyGUI::RenderManager::getInstance().getTexture(image+"_fog") != 0) ? image+"_fog" @@ -112,22 +225,10 @@ namespace MWGui : ""); } } - notifyMapChanged (); - } - - void LocalMapBase::onMarkerFocused (MyGUI::Widget* w1, MyGUI::Widget* w2) - { - // Workaround to not make the marker visible if it's under fog of war - applyFogOfWar (); + redraw(); } - void LocalMapBase::onMarkerUnfocused (MyGUI::Widget* w1, MyGUI::Widget* w2) - { - // Workaround to not make the marker visible if it's under fog of war - applyFogOfWar (); - } - - MyGUI::IntPoint LocalMapBase::getMarkerPosition(float worldX, float worldY, MarkerPosition& markerPos) + MyGUI::IntPoint LocalMapBase::getMarkerPosition(float worldX, float worldY, MarkerUserData& markerPos) { MyGUI::IntPoint widgetPos; // normalized cell coordinates @@ -139,7 +240,6 @@ namespace MWGui { int cellX, cellY; MWBase::Environment::get().getWorld()->positionToIndex(worldX, worldY, cellX, cellY); - const int cellSize = 8192; nX = (worldX - cellSize * cellX) / cellSize; // Image space is -Y up, cells are Y up nY = 1 - (worldY - cellSize * cellY) / cellSize; @@ -150,21 +250,21 @@ namespace MWGui markerPos.cellX = cellX; markerPos.cellY = cellY; - widgetPos = MyGUI::IntPoint(nX * 512 + (1+cellDx) * 512, - nY * 512 - (cellDy-1) * 512); + widgetPos = MyGUI::IntPoint(nX * mMapWidgetSize + (1+cellDx) * mMapWidgetSize, + nY * mMapWidgetSize - (cellDy-1) * mMapWidgetSize); } else { int cellX, cellY; Ogre::Vector2 worldPos (worldX, worldY); - MWBase::Environment::get().getWorld ()->getInteriorMapPosition (worldPos, nX, nY, cellX, cellY); + MWBase::Environment::get().getWorld ()->worldToInteriorMapPosition (worldPos, nX, nY, cellX, cellY); markerPos.cellX = cellX; markerPos.cellY = cellY; // Image space is -Y up, cells are Y up - widgetPos = MyGUI::IntPoint(nX * 512 + (1+(cellX-mCurX)) * 512, - nY * 512 + (1-(cellY-mCurY)) * 512); + widgetPos = MyGUI::IntPoint(nX * mMapWidgetSize + (1+(cellX-mCurX)) * mMapWidgetSize, + nY * mMapWidgetSize + (1-(cellY-mCurY)) * mMapWidgetSize); } markerPos.nX = nX; @@ -172,6 +272,53 @@ namespace MWGui return widgetPos; } + void LocalMapBase::updateCustomMarkers() + { + for (std::vector::iterator it = mCustomMarkerWidgets.begin(); it != mCustomMarkerWidgets.end(); ++it) + MyGUI::Gui::getInstance().destroyWidget(*it); + mCustomMarkerWidgets.clear(); + + for (std::vector::const_iterator it = mCustomMarkers.begin(); it != mCustomMarkers.end(); ++it) + { + const ESM::CustomMarker& marker = *it; + + if (marker.mCell.mPaged != !mInterior) + continue; + if (mInterior) + { + if (marker.mCell.mWorldspace != mPrefix) + continue; + } + else + { + if (std::abs(marker.mCell.mIndex.mX - mCurX) > 1) + continue; + if (std::abs(marker.mCell.mIndex.mY - mCurY) > 1) + continue; + } + + MarkerUserData markerPos; + MyGUI::IntPoint widgetPos = getMarkerPosition(marker.mWorldX, marker.mWorldY, markerPos); + + MyGUI::IntCoord widgetCoord(widgetPos.left - 4, + widgetPos.top - 4, + 8, 8); + MarkerWidget* markerWidget = mLocalMap->createWidget("MarkerButton", + widgetCoord, MyGUI::Align::Default); + markerWidget->setDepth(Local_MarkerAboveFogLayer); + markerWidget->setUserString("ToolTipType", "Layout"); + markerWidget->setUserString("ToolTipLayout", "TextToolTipOneLine"); + markerWidget->setUserString("Caption_TextOneLine", MyGUI::TextIterator::toTagsString(marker.mNote)); + markerWidget->setNormalColour(MyGUI::Colour(1.0,0.3,0.3)); + markerWidget->setHoverColour(MyGUI::Colour(1.0,0.5,0.5)); + markerWidget->setUserData(marker); + markerWidget->setNeedMouseFocus(true); + customMarkerCreated(markerWidget); + mCustomMarkerWidgets.push_back(markerWidget); + } + redraw(); + } + void LocalMapBase::setActiveCell(const int x, const int y, bool interior) { if (x==mCurX && y==mCurY && mInterior==interior && !mChanged) @@ -182,6 +329,9 @@ namespace MWGui mInterior = interior; mChanged = false; + applyFogOfWar(); + + // clear all previous door markers for (std::vector::iterator it = mDoorMarkerWidgets.begin(); it != mDoorMarkerWidgets.end(); ++it) MyGUI::Gui::getInstance().destroyWidget(*it); @@ -193,8 +343,8 @@ namespace MWGui for (int my=0; my<3; ++my) { // map - std::string image = mPrefix+"_"+ boost::lexical_cast(x + (mx-1)) + "_" - + boost::lexical_cast(y + (-1*(my-1))); + std::string image = mPrefix+"_"+ MyGUI::utility::toString(x + (mx-1)) + "_" + + MyGUI::utility::toString(y + (-1*(my-1))); MyGUI::ImageBox* box = mMapWidgets[my + 3*mx]; @@ -232,57 +382,62 @@ namespace MWGui { MWBase::World::DoorMarker marker = *it; - MarkerPosition markerPos; - MyGUI::IntPoint widgetPos = getMarkerPosition(marker.x, marker.y, markerPos); + std::vector destNotes; + for (std::vector::const_iterator it = mCustomMarkers.begin(); it != mCustomMarkers.end(); ++it) + { + if (it->mCell == marker.dest) + destNotes.push_back(it->mNote); + } + + MarkerUserData data; + data.notes = destNotes; + data.caption = marker.name; + MyGUI::IntPoint widgetPos = getMarkerPosition(marker.x, marker.y, data); MyGUI::IntCoord widgetCoord(widgetPos.left - 4, widgetPos.top - 4, 8, 8); ++counter; - MyGUI::Button* markerWidget = mLocalMap->createWidget("ButtonImage", + MarkerWidget* markerWidget = mLocalMap->createWidget("MarkerButton", widgetCoord, MyGUI::Align::Default); - markerWidget->setImageResource("DoorMarker"); - markerWidget->setUserString("ToolTipType", "Layout"); - markerWidget->setUserString("ToolTipLayout", "TextToolTipOneLine"); - markerWidget->setUserString("Caption_TextOneLine", marker.name); - markerWidget->eventMouseSetFocus += MyGUI::newDelegate(this, &LocalMapBase::onMarkerFocused); - markerWidget->eventMouseLostFocus += MyGUI::newDelegate(this, &LocalMapBase::onMarkerUnfocused); + markerWidget->setNormalColour(MyGUI::Colour::parse(MyGUI::LanguageManager::getInstance().replaceTags("#{fontcolour=normal}"))); + markerWidget->setHoverColour(MyGUI::Colour::parse(MyGUI::LanguageManager::getInstance().replaceTags("#{fontcolour=normal_over}"))); + markerWidget->setDepth(Local_MarkerLayer); + markerWidget->setNeedMouseFocus(true); // Used by tooltips to not show the tooltip if marker is hidden by fog of war - markerWidget->setUserString("IsMarker", "true"); - markerWidget->setUserData(markerPos); + markerWidget->setUserString("ToolTipType", "MapMarker"); + + markerWidget->setUserData(data); + doorMarkerCreated(markerWidget); mDoorMarkerWidgets.push_back(markerWidget); } - updateMarkers(); - - applyFogOfWar(); - - // set the compass texture again, because MyGUI determines sorting of ImageBox widgets - // based on the last setImageTexture call - std::string tex = "textures\\compass.dds"; - mCompass->setImageTexture(""); - mCompass->setImageTexture(tex); + updateMagicMarkers(); + updateCustomMarkers(); } - - void LocalMapBase::setPlayerPos(const float x, const float y) + void LocalMapBase::redraw() { - updateMarkers(); - - if (x == mLastPositionX && y == mLastPositionY) - return; + // Redraw children in proper order + mLocalMap->getParent()->_updateChilds(); + } - notifyPlayerUpdate (); + void LocalMapBase::setPlayerPos(int cellX, int cellY, const float nx, const float ny) + { + MyGUI::IntPoint pos(mMapWidgetSize+nx*mMapWidgetSize-16, mMapWidgetSize+ny*mMapWidgetSize-16); + pos.left += (cellX - mCurX) * mMapWidgetSize; + pos.top -= (cellY - mCurY) * mMapWidgetSize; - MyGUI::IntSize size = mLocalMap->getCanvasSize(); - MyGUI::IntPoint middle = MyGUI::IntPoint((1/3.f + x/3.f)*size.width,(1/3.f + y/3.f)*size.height); - MyGUI::IntCoord viewsize = mLocalMap->getCoord(); - MyGUI::IntPoint pos(0.5*viewsize.width - middle.left, 0.5*viewsize.height - middle.top); - mLocalMap->setViewOffset(pos); + if (pos != mCompass->getPosition()) + { + notifyPlayerUpdate (); - mCompass->setPosition(MyGUI::IntPoint(512+x*512-16, 512+y*512-16)); - mLastPositionX = x; - mLastPositionY = y; + mCompass->setPosition(pos); + MyGUI::IntPoint middle (pos.left+16, pos.top+16); + MyGUI::IntCoord viewsize = mLocalMap->getCoord(); + MyGUI::IntPoint viewOffset(0.5*viewsize.width - middle.left, 0.5*viewsize.height - middle.top); + mLocalMap->setViewOffset(viewOffset); + } } void LocalMapBase::setPlayerDir(const float x, const float y) @@ -334,7 +489,7 @@ namespace MWGui for (std::vector::iterator it = markers.begin(); it != markers.end(); ++it) { const ESM::Position& worldPos = it->getRefData().getPosition(); - MarkerPosition markerPos; + MarkerUserData markerPos; MyGUI::IntPoint widgetPos = getMarkerPosition(worldPos.pos[0], worldPos.pos[1], markerPos); MyGUI::IntCoord widgetCoord(widgetPos.left - 4, widgetPos.top - 4, @@ -342,11 +497,11 @@ namespace MWGui ++counter; MyGUI::ImageBox* markerWidget = mLocalMap->createWidget("ImageBox", widgetCoord, MyGUI::Align::Default); + markerWidget->setDepth(Local_MarkerAboveFogLayer); markerWidget->setImageTexture(markerTexture); - markerWidget->setUserString("IsMarker", "true"); - markerWidget->setUserData(markerPos); markerWidget->setColour(markerColour); - mMarkerWidgets.push_back(markerWidget); + markerWidget->setNeedMouseFocus(false); + mMagicMarkerWidgets.push_back(markerWidget); } } @@ -357,16 +512,16 @@ namespace MWGui if (mMarkerUpdateTimer >= 0.25) { mMarkerUpdateTimer = 0; - updateMarkers(); + updateMagicMarkers(); } } - void LocalMapBase::updateMarkers() + void LocalMapBase::updateMagicMarkers() { // clear all previous markers - for (std::vector::iterator it = mMarkerWidgets.begin(); it != mMarkerWidgets.end(); ++it) + for (std::vector::iterator it = mMagicMarkerWidgets.begin(); it != mMagicMarkerWidgets.end(); ++it) MyGUI::Gui::getInstance().destroyWidget(*it); - mMarkerWidgets.clear(); + mMagicMarkerWidgets.clear(); addDetectionMarkers(MWBase::World::Detect_Creature); addDetectionMarkers(MWBase::World::Detect_Key); @@ -379,29 +534,48 @@ namespace MWGui if (markedCell && markedCell->isExterior() == !mInterior && (!mInterior || Misc::StringUtils::ciEqual(markedCell->getCell()->mName, mPrefix))) { - MarkerPosition markerPos; + MarkerUserData markerPos; MyGUI::IntPoint widgetPos = getMarkerPosition(markedPosition.pos[0], markedPosition.pos[1], markerPos); MyGUI::IntCoord widgetCoord(widgetPos.left - 4, widgetPos.top - 4, 8, 8); MyGUI::ImageBox* markerWidget = mLocalMap->createWidget("ImageBox", widgetCoord, MyGUI::Align::Default); + markerWidget->setDepth(Local_MarkerAboveFogLayer); markerWidget->setImageTexture("textures\\menu_map_smark.dds"); - markerWidget->setUserString("IsMarker", "true"); - markerWidget->setUserData(markerPos); - mMarkerWidgets.push_back(markerWidget); + markerWidget->setNeedMouseFocus(false); + mMagicMarkerWidgets.push_back(markerWidget); } + + redraw(); } // ------------------------------------------------------------------------------------------ - MapWindow::MapWindow(DragAndDrop* drag, const std::string& cacheDir) + MapWindow::MapWindow(CustomMarkerCollection &customMarkers, DragAndDrop* drag, const std::string& cacheDir) : WindowPinnableBase("openmw_map_window.layout") , NoDrop(drag, mMainWidget) + , LocalMapBase(customMarkers) , mGlobal(false) , mGlobalMap(0) , mGlobalMapRender(0) - { + , mEditNoteDialog() + , mEventBoxGlobal(NULL) + , mEventBoxLocal(NULL) + , mGlobalMapImage(NULL) + , mGlobalMapOverlay(NULL) + { + static bool registered = false; + if (!registered) + { + MyGUI::FactoryManager::getInstance().registerFactory("Widget"); + registered = true; + } + + mEditNoteDialog.setVisible(false); + mEditNoteDialog.eventOkClicked += MyGUI::newDelegate(this, &MapWindow::onNoteEditOk); + mEditNoteDialog.eventDeleteClicked += MyGUI::newDelegate(this, &MapWindow::onNoteEditDelete); + setCoord(500,0,320,300); getWidget(mLocalMap, "LocalMap"); @@ -411,6 +585,14 @@ namespace MWGui getWidget(mPlayerArrowLocal, "CompassLocal"); getWidget(mPlayerArrowGlobal, "CompassGlobal"); + mPlayerArrowGlobal->setDepth(Global_CompassLayer); + mPlayerArrowGlobal->setNeedMouseFocus(false); + mGlobalMapImage->setDepth(Global_MapLayer); + mGlobalMapOverlay->setDepth(Global_ExploreOverlayLayer); + + mLastScrollWindowCoordinates = mLocalMap->getCoord(); + mLocalMap->eventChangeCoord += MyGUI::newDelegate(this, &MapWindow::onChangeScrollWindowCoord); + mGlobalMap->setVisible (false); getWidget(mButton, "WorldButton"); @@ -420,11 +602,105 @@ namespace MWGui getWidget(mEventBoxGlobal, "EventBoxGlobal"); mEventBoxGlobal->eventMouseDrag += MyGUI::newDelegate(this, &MapWindow::onMouseDrag); mEventBoxGlobal->eventMouseButtonPressed += MyGUI::newDelegate(this, &MapWindow::onDragStart); + mEventBoxGlobal->setDepth(Global_ExploreOverlayLayer); + getWidget(mEventBoxLocal, "EventBoxLocal"); mEventBoxLocal->eventMouseDrag += MyGUI::newDelegate(this, &MapWindow::onMouseDrag); mEventBoxLocal->eventMouseButtonPressed += MyGUI::newDelegate(this, &MapWindow::onDragStart); + mEventBoxLocal->eventMouseButtonDoubleClick += MyGUI::newDelegate(this, &MapWindow::onMapDoubleClicked); + + LocalMapBase::init(mLocalMap, mPlayerArrowLocal, Settings::Manager::getInt("local map widget size", "Map")); } + + void MapWindow::onNoteEditOk() + { + if (mEditNoteDialog.getDeleteButtonShown()) + mCustomMarkers.updateMarker(mEditingMarker, mEditNoteDialog.getText()); + else + { + mEditingMarker.mNote = mEditNoteDialog.getText(); + mCustomMarkers.addMarker(mEditingMarker); + } - LocalMapBase::init(mLocalMap, mPlayerArrowLocal, this); + mEditNoteDialog.setVisible(false); + } + + void MapWindow::onNoteEditDelete() + { + ConfirmationDialog* confirmation = MWBase::Environment::get().getWindowManager()->getConfirmationDialog(); + confirmation->open("#{sDeleteNote}", "#{sYes}", "#{sNo}"); + confirmation->eventCancelClicked.clear(); + confirmation->eventOkClicked.clear(); + confirmation->eventOkClicked += MyGUI::newDelegate(this, &MapWindow::onNoteEditDeleteConfirm); + } + + void MapWindow::onNoteEditDeleteConfirm() + { + mCustomMarkers.deleteMarker(mEditingMarker); + + mEditNoteDialog.setVisible(false); + } + + void MapWindow::onCustomMarkerDoubleClicked(MyGUI::Widget *sender) + { + mEditingMarker = *sender->getUserData(); + mEditNoteDialog.setText(mEditingMarker.mNote); + mEditNoteDialog.showDeleteButton(true); + mEditNoteDialog.setVisible(true); + } + + void MapWindow::onMapDoubleClicked(MyGUI::Widget *sender) + { + MyGUI::IntPoint clickedPos = MyGUI::InputManager::getInstance().getMousePosition(); + + MyGUI::IntPoint widgetPos = clickedPos - mEventBoxLocal->getAbsolutePosition(); + int x = int(widgetPos.left/float(mMapWidgetSize))-1; + int y = (int(widgetPos.top/float(mMapWidgetSize))-1)*-1; + float nX = widgetPos.left/float(mMapWidgetSize) - int(widgetPos.left/float(mMapWidgetSize)); + float nY = widgetPos.top/float(mMapWidgetSize) - int(widgetPos.top/float(mMapWidgetSize)); + x += mCurX; + y += mCurY; + + Ogre::Vector2 worldPos; + if (mInterior) + { + worldPos = MWBase::Environment::get().getWorld()->interiorMapToWorldPosition(nX, nY, x, y); + } + else + { + worldPos.x = (x + nX) * cellSize; + worldPos.y = (y + (1.0-nY)) * cellSize; + } + + mEditingMarker.mWorldX = worldPos.x; + mEditingMarker.mWorldY = worldPos.y; + + mEditingMarker.mCell.mPaged = !mInterior; + if (mInterior) + mEditingMarker.mCell.mWorldspace = LocalMapBase::mPrefix; + else + { + mEditingMarker.mCell.mWorldspace = "sys::default"; + mEditingMarker.mCell.mIndex.mX = x; + mEditingMarker.mCell.mIndex.mY = y; + } + + mEditNoteDialog.setVisible(true); + mEditNoteDialog.showDeleteButton(false); + mEditNoteDialog.setText(""); + } + + void MapWindow::onChangeScrollWindowCoord(MyGUI::Widget* sender) + { + MyGUI::IntCoord currentCoordinates = sender->getCoord(); + + MyGUI::IntPoint currentViewPortCenter = MyGUI::IntPoint(currentCoordinates.width / 2, currentCoordinates.height / 2); + MyGUI::IntPoint lastViewPortCenter = MyGUI::IntPoint(mLastScrollWindowCoordinates.width / 2, mLastScrollWindowCoordinates.height / 2); + MyGUI::IntPoint viewPortCenterDiff = currentViewPortCenter - lastViewPortCenter; + + mLocalMap->setViewOffset(mLocalMap->getViewOffset() + viewPortCenterDiff); + mGlobalMap->setViewOffset(mGlobalMap->getViewOffset() + viewPortCenterDiff); + + mLastScrollWindowCoordinates = currentCoordinates; } void MapWindow::renderGlobalMap(Loading::Listener* loadingListener) @@ -450,34 +726,32 @@ namespace MWGui void MapWindow::addVisitedLocation(const std::string& name, int x, int y) { - float worldX, worldY; - mGlobalMapRender->cellTopLeftCornerToImageSpace (x, y, worldX, worldY); - - MyGUI::IntCoord widgetCoord( - worldX * mGlobalMapRender->getWidth()+6, - worldY * mGlobalMapRender->getHeight()+6, - 12, 12); - - static int _counter=0; - MyGUI::Button* markerWidget = mGlobalMapOverlay->createWidget("ButtonImage", - widgetCoord, MyGUI::Align::Default); - markerWidget->setImageResource("DoorMarker"); - markerWidget->setUserString("ToolTipType", "Layout"); - markerWidget->setUserString("ToolTipLayout", "TextToolTipOneLine"); - markerWidget->setUserString("Caption_TextOneLine", name); - ++_counter; - - markerWidget = mEventBoxGlobal->createWidget("", - widgetCoord, MyGUI::Align::Default); - markerWidget->setNeedMouseFocus (true); - markerWidget->setUserString("ToolTipType", "Layout"); - markerWidget->setUserString("ToolTipLayout", "TextToolTipOneLine"); - markerWidget->setUserString("Caption_TextOneLine", name); - CellId cell; cell.first = x; cell.second = y; - mMarkers.push_back(cell); + if (mMarkers.insert(cell).second) + { + float worldX, worldY; + mGlobalMapRender->cellTopLeftCornerToImageSpace (x, y, worldX, worldY); + + int markerSize = 12; + int offset = mGlobalMapRender->getCellSize()/2 - markerSize/2; + MyGUI::IntCoord widgetCoord( + worldX * mGlobalMapRender->getWidth()+offset, + worldY * mGlobalMapRender->getHeight()+offset, + markerSize, markerSize); + + MyGUI::Widget* markerWidget = mGlobalMap->createWidget("MarkerButton", + widgetCoord, MyGUI::Align::Default); + markerWidget->setNeedMouseFocus(true); + markerWidget->setColour(MyGUI::Colour::parse(MyGUI::LanguageManager::getInstance().replaceTags("#{fontcolour=normal}"))); + markerWidget->setUserString("ToolTipType", "Layout"); + markerWidget->setUserString("ToolTipLayout", "TextToolTipOneLine"); + markerWidget->setUserString("Caption_TextOneLine", name); + markerWidget->setDepth(Global_MarkerLayer); + markerWidget->eventMouseDrag += MyGUI::newDelegate(this, &MapWindow::onMouseDrag); + markerWidget->eventMouseButtonPressed += MyGUI::newDelegate(this, &MapWindow::onDragStart); + } } void MapWindow::cellExplored(int x, int y) @@ -536,18 +810,15 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->setMinimapVisibility(!mPinned); } - void MapWindow::open() + void MapWindow::onTitleDoubleClicked() { - // force markers to foreground - for (unsigned int i=0; igetChildCount (); ++i) - { - if (mGlobalMapOverlay->getChildAt (i)->getName().substr(0,4) == "Door") - mGlobalMapOverlay->getChildAt (i)->castType()->setImageResource("DoorMarker"); - } + if (!mPinned) + MWBase::Environment::get().getWindowManager()->toggleVisible(GW_Map); + } + void MapWindow::open() + { globalMapUpdatePlayer(); - - mPlayerArrowGlobal->setImageTexture ("textures\\compass.dds"); } void MapWindow::globalMapUpdatePlayer () @@ -556,8 +827,6 @@ namespace MWGui if (MWBase::Environment::get().getWorld ()->isCellExterior ()) { Ogre::Vector3 pos = MWBase::Environment::get().getWorld ()->getPlayerPtr().getRefData ().getBaseNode ()->_getDerivedPosition (); - Ogre::Quaternion orient = MWBase::Environment::get().getWorld ()->getPlayerPtr().getRefData ().getBaseNode ()->_getDerivedOrientation (); - Ogre::Vector2 dir (orient.yAxis ().x, orient.yAxis().y); float worldX, worldY; mGlobalMapRender->worldPosToImageSpace (pos.x, pos.y, worldX, worldY); @@ -566,12 +835,6 @@ namespace MWGui mPlayerArrowGlobal->setPosition(MyGUI::IntPoint(worldX - 16, worldY - 16)); - MyGUI::ISubWidget* main = mPlayerArrowGlobal->getSubWidgetMain(); - MyGUI::RotatingSkin* rotatingSubskin = main->castType(); - rotatingSubskin->setCenter(MyGUI::IntPoint(16,16)); - float angle = std::atan2(dir.x, dir.y); - rotatingSubskin->setAngle(angle); - // set the view offset so that player is in the center MyGUI::IntSize viewsize = mGlobalMap->getSize(); MyGUI::IntPoint viewoffs(0.5*viewsize.width - worldX, 0.5*viewsize.height - worldY); @@ -584,20 +847,6 @@ namespace MWGui globalMapUpdatePlayer (); } - void MapWindow::notifyMapChanged () - { - // workaround to prevent the map from drawing on top of the button - MyGUI::IntCoord oldCoord = mButton->getCoord (); - MyGUI::Gui::getInstance().destroyWidget (mButton); - mButton = mMainWidget->createWidget("MW_Button", - oldCoord, MyGUI::Align::Bottom | MyGUI::Align::Right); - mButton->setProperty ("ExpandDirection", "Left"); - - mButton->eventMouseButtonClick += MyGUI::newDelegate(this, &MapWindow::onWorldButtonClicked); - mButton->setCaptionWithReplacing( mGlobal ? "#{sLocal}" : - "#{sWorld}"); - } - void MapWindow::setGlobalMapPlayerPosition(float worldX, float worldY) { float x, y; @@ -613,15 +862,23 @@ namespace MWGui mGlobalMap->setViewOffset(viewoffs); } + void MapWindow::setGlobalMapPlayerDir(const float x, const float y) + { + MyGUI::ISubWidget* main = mPlayerArrowGlobal->getSubWidgetMain(); + MyGUI::RotatingSkin* rotatingSubskin = main->castType(); + rotatingSubskin->setCenter(MyGUI::IntPoint(16,16)); + float angle = std::atan2(x,y); + rotatingSubskin->setAngle(angle); + } + void MapWindow::clear() { mMarkers.clear(); mGlobalMapRender->clear(); + mChanged = true; while (mEventBoxGlobal->getChildCount()) MyGUI::Gui::getInstance().destroyWidget(mEventBoxGlobal->getChildAt(0)); - while (mGlobalMapOverlay->getChildCount()) - MyGUI::Gui::getInstance().destroyWidget(mGlobalMapOverlay->getChildAt(0)); } void MapWindow::write(ESM::ESMWriter &writer, Loading::Listener& progress) @@ -634,10 +891,9 @@ namespace MWGui writer.startRecord(ESM::REC_GMAP); map.save(writer); writer.endRecord(ESM::REC_GMAP); - progress.increaseProgress(); } - void MapWindow::readRecord(ESM::ESMReader &reader, int32_t type) + void MapWindow::readRecord(ESM::ESMReader &reader, uint32_t type) { if (type == ESM::REC_GMAP) { @@ -646,7 +902,7 @@ namespace MWGui mGlobalMapRender->read(map); - for (std::vector::iterator it = map.mMarkers.begin(); it != map.mMarkers.end(); ++it) + for (std::set::iterator it = map.mMarkers.begin(); it != map.mMarkers.end(); ++it) { const ESM::Cell* cell = MWBase::Environment::get().getWorld()->getStore().get().search(it->first, it->second); if (cell && !cell->mName.empty()) @@ -654,4 +910,89 @@ namespace MWGui } } } + + void MapWindow::setAlpha(float alpha) + { + NoDrop::setAlpha(alpha); + // can't allow showing map with partial transparency, as the fog of war will also go transparent + // and reveal parts of the map you shouldn't be able to see + for (std::vector::iterator it = mMapWidgets.begin(); it != mMapWidgets.end(); ++it) + (*it)->setVisible(alpha == 1); + } + + void MapWindow::customMarkerCreated(MyGUI::Widget *marker) + { + marker->eventMouseDrag += MyGUI::newDelegate(this, &MapWindow::onMouseDrag); + marker->eventMouseButtonPressed += MyGUI::newDelegate(this, &MapWindow::onDragStart); + marker->eventMouseButtonDoubleClick += MyGUI::newDelegate(this, &MapWindow::onCustomMarkerDoubleClicked); + } + + void MapWindow::doorMarkerCreated(MyGUI::Widget *marker) + { + marker->eventMouseDrag += MyGUI::newDelegate(this, &MapWindow::onMouseDrag); + marker->eventMouseButtonPressed += MyGUI::newDelegate(this, &MapWindow::onDragStart); + } + + // ------------------------------------------------------------------- + + EditNoteDialog::EditNoteDialog() + : WindowModal("openmw_edit_note.layout") + { + getWidget(mOkButton, "OkButton"); + getWidget(mCancelButton, "CancelButton"); + getWidget(mDeleteButton, "DeleteButton"); + getWidget(mTextEdit, "TextEdit"); + + mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &EditNoteDialog::onCancelButtonClicked); + mOkButton->eventMouseButtonClick += MyGUI::newDelegate(this, &EditNoteDialog::onOkButtonClicked); + mDeleteButton->eventMouseButtonClick += MyGUI::newDelegate(this, &EditNoteDialog::onDeleteButtonClicked); + } + + void EditNoteDialog::showDeleteButton(bool show) + { + mDeleteButton->setVisible(show); + } + + bool EditNoteDialog::getDeleteButtonShown() + { + return mDeleteButton->getVisible(); + } + + void EditNoteDialog::setText(const std::string &text) + { + mTextEdit->setCaption(MyGUI::TextIterator::toTagsString(text)); + } + + std::string EditNoteDialog::getText() + { + return MyGUI::TextIterator::getOnlyText(mTextEdit->getCaption()); + } + + void EditNoteDialog::open() + { + WindowModal::open(); + center(); + MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mTextEdit); + } + + void EditNoteDialog::exit() + { + setVisible(false); + } + + void EditNoteDialog::onCancelButtonClicked(MyGUI::Widget *sender) + { + setVisible(false); + } + + void EditNoteDialog::onOkButtonClicked(MyGUI::Widget *sender) + { + eventOkClicked(); + } + + void EditNoteDialog::onDeleteButtonClicked(MyGUI::Widget *sender) + { + eventDeleteClicked(); + } + } diff --git a/apps/openmw/mwgui/mapwindow.hpp b/apps/openmw/mwgui/mapwindow.hpp index c73e5d7a1..a80b3e4c5 100644 --- a/apps/openmw/mwgui/mapwindow.hpp +++ b/apps/openmw/mwgui/mapwindow.hpp @@ -5,6 +5,10 @@ #include "windowpinnablebase.hpp" +#include + +#include + namespace MWRender { class GlobalMap; @@ -23,29 +27,53 @@ namespace Loading namespace MWGui { + + class CustomMarkerCollection + { + public: + void addMarker(const ESM::CustomMarker& marker, bool triggerEvent=true); + void deleteMarker (const ESM::CustomMarker& marker); + void updateMarker(const ESM::CustomMarker& marker, const std::string& newNote); + + void clear(); + + size_t size() const; + + std::vector::const_iterator begin() const; + std::vector::const_iterator end() const; + + typedef MyGUI::delegates::CMultiDelegate0 EventHandle_Void; + EventHandle_Void eventMarkersChanged; + + private: + std::vector mMarkers; + }; + class LocalMapBase { public: - LocalMapBase(); + LocalMapBase(CustomMarkerCollection& markers); virtual ~LocalMapBase(); - void init(MyGUI::ScrollView* widget, MyGUI::ImageBox* compass, OEngine::GUI::Layout* layout, bool mapDragAndDrop=false); + void init(MyGUI::ScrollView* widget, MyGUI::ImageBox* compass, int mapWidgetSize); void setCellPrefix(const std::string& prefix); void setActiveCell(const int x, const int y, bool interior=false); void setPlayerDir(const float x, const float y); - void setPlayerPos(const float x, const float y); + void setPlayerPos(int cellX, int cellY, const float nx, const float ny); void onFrame(float dt); bool toggleFogOfWar(); - struct MarkerPosition + struct MarkerUserData { bool interior; int cellX; int cellY; float nX; float nY; + std::vector notes; + std::string caption; }; protected: @@ -57,48 +85,81 @@ namespace MWGui bool mChanged; bool mFogOfWar; + int mMapWidgetSize; + + // Stores markers that were placed by a player. May be shared between multiple map views. + CustomMarkerCollection& mCustomMarkers; + std::vector mMapWidgets; std::vector mFogWidgets; // Keep track of created marker widgets, just to easily remove them later. - std::vector mDoorMarkerWidgets; // Doors - std::vector mMarkerWidgets; // Other markers + std::vector mDoorMarkerWidgets; + std::vector mMagicMarkerWidgets; + std::vector mCustomMarkerWidgets; - void applyFogOfWar(); + void updateCustomMarkers(); - void onMarkerFocused(MyGUI::Widget* w1, MyGUI::Widget* w2); - void onMarkerUnfocused(MyGUI::Widget* w1, MyGUI::Widget* w2); + void applyFogOfWar(); - MyGUI::IntPoint getMarkerPosition (float worldX, float worldY, MarkerPosition& markerPos); + MyGUI::IntPoint getMarkerPosition (float worldX, float worldY, MarkerUserData& markerPos); virtual void notifyPlayerUpdate() {} virtual void notifyMapChanged() {} - // Update markers (Detect X effects, Mark/Recall effects) - // Note, door markers are handled in setActiveCell - void updateMarkers(); + virtual void customMarkerCreated(MyGUI::Widget* marker) {} + virtual void doorMarkerCreated(MyGUI::Widget* marker) {} + + void updateMagicMarkers(); void addDetectionMarkers(int type); - OEngine::GUI::Layout* mLayout; + void redraw(); float mMarkerUpdateTimer; - bool mMapDragAndDrop; - - float mLastPositionX; - float mLastPositionY; float mLastDirectionX; float mLastDirectionY; }; + class EditNoteDialog : public MWGui::WindowModal + { + public: + EditNoteDialog(); + + virtual void open(); + virtual void exit(); + + void showDeleteButton(bool show); + bool getDeleteButtonShown(); + void setText(const std::string& text); + std::string getText(); + + typedef MyGUI::delegates::CMultiDelegate0 EventHandle_Void; + + EventHandle_Void eventDeleteClicked; + EventHandle_Void eventOkClicked; + + private: + void onCancelButtonClicked(MyGUI::Widget* sender); + void onOkButtonClicked(MyGUI::Widget* sender); + void onDeleteButtonClicked(MyGUI::Widget* sender); + + MyGUI::TextBox* mTextEdit; + MyGUI::Button* mOkButton; + MyGUI::Button* mCancelButton; + MyGUI::Button* mDeleteButton; + }; + class MapWindow : public MWGui::WindowPinnableBase, public LocalMapBase, public NoDrop { public: - MapWindow(DragAndDrop* drag, const std::string& cacheDir); + MapWindow(CustomMarkerCollection& customMarkers, DragAndDrop* drag, const std::string& cacheDir); virtual ~MapWindow(); void setCellName(const std::string& cellName); + virtual void setAlpha(float alpha); + void renderGlobalMap(Loading::Listener* loadingListener); // adds the marker to the global map @@ -108,6 +169,7 @@ namespace MWGui void cellExplored(int x, int y); void setGlobalMapPlayerPosition (float worldX, float worldY); + void setGlobalMapPlayerDir(const float x, const float y); virtual void open(); @@ -117,13 +179,19 @@ namespace MWGui void clear(); void write (ESM::ESMWriter& writer, Loading::Listener& progress); - void readRecord (ESM::ESMReader& reader, int32_t type); + void readRecord (ESM::ESMReader& reader, uint32_t type); private: void onDragStart(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id); void onMouseDrag(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id); void onWorldButtonClicked(MyGUI::Widget* _sender); - + void onMapDoubleClicked(MyGUI::Widget* sender); + void onCustomMarkerDoubleClicked(MyGUI::Widget* sender); + void onNoteEditOk(); + void onNoteEditDelete(); + void onNoteEditDeleteConfirm(); + void onNoteDoubleClicked(MyGUI::Widget* sender); + void onChangeScrollWindowCoord(MyGUI::Widget* sender); void globalMapUpdatePlayer(); MyGUI::ScrollView* mGlobalMap; @@ -135,9 +203,11 @@ namespace MWGui MyGUI::IntPoint mLastDragPos; bool mGlobal; + MyGUI::IntCoord mLastScrollWindowCoordinates; + // Markers on global map typedef std::pair CellId; - std::vector mMarkers; + std::set mMarkers; // Cells that should be explored in the next frame (i.e. their map revealed on the global map) // We can't do this immediately, because the map update is not immediate either (see mNeedMapUpdate in scene.cpp) @@ -148,11 +218,16 @@ namespace MWGui MWRender::GlobalMap* mGlobalMapRender; - protected: + EditNoteDialog mEditNoteDialog; + ESM::CustomMarker mEditingMarker; + virtual void onPinToggled(); + virtual void onTitleDoubleClicked(); + + virtual void doorMarkerCreated(MyGUI::Widget* marker); + virtual void customMarkerCreated(MyGUI::Widget *marker); virtual void notifyPlayerUpdate(); - virtual void notifyMapChanged(); }; } diff --git a/apps/openmw/mwgui/merchantrepair.cpp b/apps/openmw/mwgui/merchantrepair.cpp index 0d1a57000..907c664b1 100644 --- a/apps/openmw/mwgui/merchantrepair.cpp +++ b/apps/openmw/mwgui/merchantrepair.cpp @@ -2,7 +2,9 @@ #include -#include +#include +#include +#include #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" @@ -10,6 +12,8 @@ #include "../mwbase/windowmanager.hpp" #include "../mwbase/soundmanager.hpp" +#include "../mwmechanics/creaturestats.hpp" + #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/esmstore.hpp" @@ -65,13 +69,13 @@ void MerchantRepair::startRepair(const MWWorld::Ptr &actor) std::string name = iter->getClass().getName(*iter) - + " - " + boost::lexical_cast(price) + + " - " + MyGUI::utility::toString(price) + MWBase::Environment::get().getWorld()->getStore().get() - .find("sgp")->getString();; + .find("sgp")->getString(); MyGUI::Button* button = - mList->createWidget("SandTextButton", + mList->createWidget(price <= playerGold ? "SandTextButton" : "SandTextButtonDisabled", // can't use setEnabled since that removes tooltip 0, currentY, 0, @@ -81,8 +85,7 @@ void MerchantRepair::startRepair(const MWWorld::Ptr &actor) currentY += 18; - button->setEnabled(price<=playerGold); - button->setUserString("Price", boost::lexical_cast(price)); + button->setUserString("Price", MyGUI::utility::toString(price)); button->setUserData(*iter); button->setCaptionWithReplacing(name); button->setSize(button->getTextSize().width,18); @@ -91,10 +94,13 @@ void MerchantRepair::startRepair(const MWWorld::Ptr &actor) button->eventMouseButtonClick += MyGUI::newDelegate(this, &MerchantRepair::onRepairButtonClick); } } + // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden + mList->setVisibleVScroll(false); mList->setCanvasSize (MyGUI::IntSize(mList->getWidth(), std::max(mList->getHeight(), currentY))); + mList->setVisibleVScroll(true); mGoldLabel->setCaptionWithReplacing("#{sGold}: " - + boost::lexical_cast(playerGold)); + + MyGUI::utility::toString(playerGold)); } void MerchantRepair::onMouseWheel(MyGUI::Widget* _sender, int _rel) @@ -119,6 +125,10 @@ void MerchantRepair::onRepairButtonClick(MyGUI::Widget *sender) { MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); + int price = MyGUI::utility::parseInt(sender->getUserString("Price")); + if (price > player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId)) + return; + // repair MWWorld::Ptr item = *sender->getUserData(); item.getCellRef().setCharge(item.getClass().getItemMaxHealth(item)); @@ -127,10 +137,13 @@ void MerchantRepair::onRepairButtonClick(MyGUI::Widget *sender) MWBase::Environment::get().getSoundManager()->playSound("Repair",1,1); - int price = boost::lexical_cast(sender->getUserString("Price")); player.getClass().getContainerStore(player).remove(MWWorld::ContainerStore::sGoldId, price, player); + // add gold to NPC trading gold pool + MWMechanics::CreatureStats& actorStats = mActor.getClass().getCreatureStats(mActor); + actorStats.setGoldPool(actorStats.getGoldPool() + price); + startRepair(mActor); } diff --git a/apps/openmw/mwgui/merchantrepair.hpp b/apps/openmw/mwgui/merchantrepair.hpp index 2f1387365..231d11089 100644 --- a/apps/openmw/mwgui/merchantrepair.hpp +++ b/apps/openmw/mwgui/merchantrepair.hpp @@ -4,8 +4,6 @@ #include "windowbase.hpp" #include "../mwworld/ptr.hpp" - - namespace MWGui { diff --git a/apps/openmw/mwgui/messagebox.cpp b/apps/openmw/mwgui/messagebox.cpp index c4b204de7..cdbcf784d 100644 --- a/apps/openmw/mwgui/messagebox.cpp +++ b/apps/openmw/mwgui/messagebox.cpp @@ -1,9 +1,18 @@ +#include "messagebox.hpp" + +#include +#include +#include +#include + #include -#include "messagebox.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/inputmanager.hpp" +#include "../mwbase/windowmanager.hpp" + +#undef MessageBox namespace MWGui { @@ -139,15 +148,11 @@ namespace MWGui return false; } - void MessageBoxManager::setMessageBoxSpeed (int speed) - { - mMessageBoxSpeed = speed; - } - - int MessageBoxManager::readPressedButton () + int MessageBoxManager::readPressedButton (bool reset) { int pressed = mLastButtonPressed; - mLastButtonPressed = -1; + if (reset) + mLastButtonPressed = -1; return pressed; } @@ -162,12 +167,11 @@ namespace MWGui , mMaxTime(0) { // defines - mBottomPadding = 20; - mNextBoxPadding = 20; + mBottomPadding = 48; + mNextBoxPadding = 4; getWidget(mMessageWidget, "message"); - mMessageWidget->setOverflowToTheLeft(true); mMessageWidget->setCaptionWithReplacing(mMessage); } @@ -183,7 +187,7 @@ namespace MWGui int MessageBox::getHeight () { - return mMainWidget->getHeight()+mNextBoxPadding; // 20 is the padding between this and the next MessageBox + return mMainWidget->getHeight()+mNextBoxPadding; } @@ -197,9 +201,9 @@ namespace MWGui WindowModal::open(); int textPadding = 10; // padding between text-widget and main-widget - int textButtonPadding = 20; // padding between the text-widget und the button-widget + int textButtonPadding = 10; // padding between the text-widget und the button-widget int buttonLeftPadding = 10; // padding between the buttons if horizontal - int buttonTopPadding = 5; // ^-- if vertical + int buttonTopPadding = 10; // ^-- if vertical int buttonPadding = 5; // padding between button label and button itself int buttonMainPadding = 10; // padding between buttons and bottom of the main widget @@ -209,7 +213,7 @@ namespace MWGui getWidget(mMessageWidget, "message"); getWidget(mButtonsWidget, "buttons"); - mMessageWidget->setOverflowToTheLeft(true); + mMessageWidget->setSize(400, mMessageWidget->getHeight()); mMessageWidget->setCaptionWithReplacing(message); MyGUI::IntSize textSize = mMessageWidget->getTextSize(); @@ -217,8 +221,8 @@ namespace MWGui MyGUI::IntSize gameWindowSize = MyGUI::RenderManager::getInstance().getViewSize(); int biggestButtonWidth = 0; - int buttonWidth = 0; int buttonsWidth = 0; + int buttonsHeight = 0; int buttonHeight = 0; MyGUI::IntCoord dummyCoord(0, 0, 0, 0); @@ -236,64 +240,120 @@ namespace MWGui mButtons.push_back(button); - buttonWidth = button->getTextSize().width + 2*buttonPadding + buttonLeftPadding; + if (buttonsWidth != 0) + buttonsWidth += buttonLeftPadding; + + int buttonWidth = button->getTextSize().width + 2*buttonPadding; buttonsWidth += buttonWidth; - buttonHeight = button->getTextSize().height + 2*buttonPadding + buttonTopPadding; + + buttonHeight = button->getTextSize().height + 2*buttonPadding; + + if (buttonsHeight != 0) + buttonsHeight += buttonTopPadding; + buttonsHeight += buttonHeight; if(buttonWidth > biggestButtonWidth) { biggestButtonWidth = buttonWidth; } } - buttonsWidth += buttonLeftPadding; MyGUI::IntSize mainWidgetSize; - // among each other - if(biggestButtonWidth > textSize.width) { - mainWidgetSize.width = biggestButtonWidth + buttonTopPadding; - } - else { + if(buttonsWidth < textSize.width) + { + // on one line mainWidgetSize.width = textSize.width + 3*textPadding; - } + mainWidgetSize.height = textPadding + textSize.height + textButtonPadding + buttonHeight + buttonMainPadding; - MyGUI::IntCoord buttonCord; - MyGUI::IntSize buttonSize(0, buttonHeight); + MyGUI::IntSize realSize = mainWidgetSize + + // To account for borders + (mMainWidget->getSize() - mMainWidget->getClientWidget()->getSize()); - int top = textButtonPadding + buttonTopPadding + textSize.height; + MyGUI::IntPoint absPos; + absPos.left = (gameWindowSize.width - realSize.width)/2; + absPos.top = (gameWindowSize.height - realSize.height)/2; - std::vector::const_iterator button; - for(button = mButtons.begin(); button != mButtons.end(); ++button) - { - buttonSize.width = (*button)->getTextSize().width + buttonPadding*2; - buttonSize.height = (*button)->getTextSize().height + buttonPadding*2; + mMainWidget->setPosition(absPos); + mMainWidget->setSize(realSize); + + MyGUI::IntCoord messageWidgetCoord; + messageWidgetCoord.left = (mainWidgetSize.width - textSize.width)/2; + messageWidgetCoord.top = textPadding; + mMessageWidget->setCoord(messageWidgetCoord); - buttonCord.top = top; - buttonCord.left = (mainWidgetSize.width - buttonSize.width)/2 - 5; // FIXME: -5 is not so nice :/ + mMessageWidget->setSize(textSize); - (*button)->setCoord(buttonCord); - (*button)->setSize(buttonSize); + MyGUI::IntCoord buttonCord; + MyGUI::IntSize buttonSize(0, buttonHeight); + int left = (mainWidgetSize.width - buttonsWidth)/2; - top += buttonSize.height + 2*buttonTopPadding; + std::vector::const_iterator button; + for(button = mButtons.begin(); button != mButtons.end(); ++button) + { + buttonCord.left = left; + buttonCord.top = messageWidgetCoord.top + textSize.height + textButtonPadding; + + buttonSize.width = (*button)->getTextSize().width + 2*buttonPadding; + buttonSize.height = (*button)->getTextSize().height + 2*buttonPadding; + + (*button)->setCoord(buttonCord); + (*button)->setSize(buttonSize); + + left += buttonSize.width + buttonLeftPadding; + } } + else + { + // among each other + if(biggestButtonWidth > textSize.width) { + mainWidgetSize.width = biggestButtonWidth + buttonTopPadding*2; + } + else { + mainWidgetSize.width = textSize.width + 3*textPadding; + } + + MyGUI::IntCoord buttonCord; + MyGUI::IntSize buttonSize(0, buttonHeight); - mainWidgetSize.height = top + buttonMainPadding; - mMainWidget->setSize(mainWidgetSize); + int top = textPadding + textSize.height + textButtonPadding; - MyGUI::IntPoint absPos; - absPos.left = (gameWindowSize.width - mainWidgetSize.width)/2; - absPos.top = (gameWindowSize.height - mainWidgetSize.height)/2; + std::vector::const_iterator button; + for(button = mButtons.begin(); button != mButtons.end(); ++button) + { + buttonSize.width = (*button)->getTextSize().width + buttonPadding*2; + buttonSize.height = (*button)->getTextSize().height + buttonPadding*2; + + buttonCord.top = top; + buttonCord.left = (mainWidgetSize.width - buttonSize.width)/2; + + (*button)->setCoord(buttonCord); + (*button)->setSize(buttonSize); + + top += buttonSize.height + buttonTopPadding; + } - mMainWidget->setPosition(absPos); + mainWidgetSize.height = textPadding + textSize.height + textButtonPadding + buttonsHeight + buttonMainPadding; + mMainWidget->setSize(mainWidgetSize + + // To account for borders + (mMainWidget->getSize() - mMainWidget->getClientWidget()->getSize())); - MyGUI::IntCoord messageWidgetCoord; - messageWidgetCoord.left = (mainWidgetSize.width - textSize.width)/2; - messageWidgetCoord.top = textPadding; - messageWidgetCoord.width = textSize.width; - messageWidgetCoord.height = textSize.height; - mMessageWidget->setCoord(messageWidgetCoord); + MyGUI::IntPoint absPos; + absPos.left = (gameWindowSize.width - mainWidgetSize.width)/2; + absPos.top = (gameWindowSize.height - mainWidgetSize.height)/2; + + mMainWidget->setPosition(absPos); + + MyGUI::IntCoord messageWidgetCoord; + messageWidgetCoord.left = (mainWidgetSize.width - textSize.width)/2; + messageWidgetCoord.top = textPadding; + messageWidgetCoord.width = textSize.width; + messageWidgetCoord.height = textSize.height; + mMessageWidget->setCoord(messageWidgetCoord); + } // Set key focus to "Ok" button std::string ok = Misc::StringUtils::lowerCase(MyGUI::LanguageManager::getInstance().replaceTags("#{sOK}")); + std::vector::const_iterator button; for(button = mButtons.begin(); button != mButtons.end(); ++button) { if(Misc::StringUtils::ciEqual((*button)->getCaption(), ok)) diff --git a/apps/openmw/mwgui/messagebox.hpp b/apps/openmw/mwgui/messagebox.hpp index 406d98c48..48a92c844 100644 --- a/apps/openmw/mwgui/messagebox.hpp +++ b/apps/openmw/mwgui/messagebox.hpp @@ -3,8 +3,6 @@ #include "windowbase.hpp" -#include "../mwbase/windowmanager.hpp" - #undef MessageBox namespace MyGUI @@ -34,9 +32,9 @@ namespace MWGui void clear(); bool removeMessageBox (MessageBox *msgbox); - void setMessageBoxSpeed (int speed); - int readPressedButton (); + /// @param reset Reset the pressed button to -1 after reading it. + int readPressedButton (bool reset=true); typedef MyGUI::delegates::CMultiDelegate1 EventHandle_Int; diff --git a/apps/openmw/mwgui/mode.hpp b/apps/openmw/mwgui/mode.hpp index a1688d2e5..db851e067 100644 --- a/apps/openmw/mwgui/mode.hpp +++ b/apps/openmw/mwgui/mode.hpp @@ -46,6 +46,7 @@ namespace MWGui GM_Loading, GM_LoadingWallpaper, + GM_Jail, GM_QuickKeysMenu }; diff --git a/apps/openmw/mwgui/pickpocketitemmodel.cpp b/apps/openmw/mwgui/pickpocketitemmodel.cpp index 230282f15..f6882ada6 100644 --- a/apps/openmw/mwgui/pickpocketitemmodel.cpp +++ b/apps/openmw/mwgui/pickpocketitemmodel.cpp @@ -6,16 +6,19 @@ namespace MWGui { - PickpocketItemModel::PickpocketItemModel(const MWWorld::Ptr& thief, ItemModel *sourceModel) + PickpocketItemModel::PickpocketItemModel(const MWWorld::Ptr& thief, ItemModel *sourceModel, bool hideItems) { mSourceModel = sourceModel; int chance = thief.getClass().getSkill(thief, ESM::Skill::Sneak); mSourceModel->update(); - for (size_t i = 0; igetItemCount(); ++i) + if (hideItems) { - if (std::rand() / static_cast(RAND_MAX) * 100 > chance) - mHiddenItems.push_back(mSourceModel->getItem(i)); + for (size_t i = 0; igetItemCount(); ++i) + { + if (std::rand() / static_cast(RAND_MAX) * 100 > chance) + mHiddenItems.push_back(mSourceModel->getItem(i)); + } } } @@ -42,11 +45,8 @@ namespace MWGui const ItemStack& item = mSourceModel->getItem(i); // Bound items may not be stolen - if (item.mBase.getCellRef().getRefId().size() > 6 - && item.mBase.getCellRef().getRefId().substr(0,6) == "bound_") - { + if (item.mFlags & ItemStack::Flag_Bound) continue; - } if (std::find(mHiddenItems.begin(), mHiddenItems.end(), item) == mHiddenItems.end() && item.mType != ItemStack::Type_Equipped) diff --git a/apps/openmw/mwgui/pickpocketitemmodel.hpp b/apps/openmw/mwgui/pickpocketitemmodel.hpp index 090d48d0e..af72119c8 100644 --- a/apps/openmw/mwgui/pickpocketitemmodel.hpp +++ b/apps/openmw/mwgui/pickpocketitemmodel.hpp @@ -10,7 +10,7 @@ namespace MWGui class PickpocketItemModel : public ProxyItemModel { public: - PickpocketItemModel (const MWWorld::Ptr& thief, ItemModel* sourceModel); + PickpocketItemModel (const MWWorld::Ptr& thief, ItemModel* sourceModel, bool hideItems=true); virtual ItemStack getItem (ModelIndex index); virtual size_t getItemCount(); virtual void update(); diff --git a/apps/openmw/mwgui/quickkeysmenu.cpp b/apps/openmw/mwgui/quickkeysmenu.cpp index 328ef21fb..a102de1e5 100644 --- a/apps/openmw/mwgui/quickkeysmenu.cpp +++ b/apps/openmw/mwgui/quickkeysmenu.cpp @@ -1,17 +1,21 @@ #include "quickkeysmenu.hpp" -#include +#include +#include +#include #include +#include #include "../mwworld/inventorystore.hpp" #include "../mwworld/class.hpp" +#include "../mwworld/player.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwmechanics/spellcasting.hpp" -#include "../mwmechanics/spells.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwgui/inventorywindow.hpp" @@ -21,7 +25,8 @@ #include "windowmanagerimp.hpp" #include "itemselection.hpp" -#include "spellwindow.hpp" +#include "spellview.hpp" + #include "itemwidget.hpp" #include "sortfilteritemmodel.hpp" @@ -50,7 +55,7 @@ namespace MWGui for (int i = 0; i < 10; ++i) { ItemWidget* button; - getWidget(button, "QuickKey" + boost::lexical_cast(i+1)); + getWidget(button, "QuickKey" + MyGUI::utility::toString(i+1)); button->eventMouseButtonClick += MyGUI::newDelegate(this, &QuickKeysMenu::onQuickKeyButtonClicked); @@ -93,7 +98,7 @@ namespace MWGui MyGUI::TextBox* textBox = key->createWidgetReal("SandText", MyGUI::FloatCoord(0,0,1,1), MyGUI::Align::Default); textBox->setTextAlign (MyGUI::Align::Center); - textBox->setCaption (boost::lexical_cast(index+1)); + textBox->setCaption (MyGUI::utility::toString(index+1)); textBox->setNeedMouseFocus (false); } @@ -226,12 +231,9 @@ namespace MWGui esmStore.get().find(spell->mEffects.mList.front().mEffectID); std::string path = effect->mIcon; - int slashPos = path.find("\\"); + int slashPos = path.rfind('\\'); path.insert(slashPos+1, "b_"); - path = std::string("icons\\") + path; - int pos = path.rfind("."); - path.erase(pos); - path.append(".dds"); + path = Misc::ResourceHelpers::correctIconPath(path); button->setFrame("textures\\menu_icon_select_magic.dds", MyGUI::IntCoord(2, 2, 40, 40)); button->setIcon(path); @@ -295,12 +297,18 @@ namespace MWGui return; store.setSelectedEnchantItem(store.end()); MWBase::Environment::get().getWindowManager()->setSelectedSpell(spellId, int(MWMechanics::getSpellSuccessChance(spellId, player))); + MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Spell); } else if (type == Type_Item) { MWWorld::Ptr item = *button->getUserData(); - MWBase::Environment::get().getWindowManager()->getInventoryWindow()->useItem(item); + MWWorld::ContainerStoreIterator rightHand = store.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); + // change draw state only if the item is in player's right hand + if (rightHand != store.end() && item == *rightHand) + { + MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Weapon); + } } else if (type == Type_MagicItem) { @@ -321,9 +329,14 @@ namespace MWGui if (!item.getClass().getEquipmentSlots(item).first.empty()) { MWBase::Environment::get().getWindowManager()->getInventoryWindow()->useItem(item); + + // make sure that item was successfully equipped + if (!store.isEquipped(item)) + return; } store.setSelectedEnchantItem(it); + MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Spell); } } @@ -418,7 +431,7 @@ namespace MWGui writer.endRecord(ESM::REC_KEYS); } - void QuickKeysMenu::readRecord(ESM::ESMReader &reader, int32_t type) + void QuickKeysMenu::readRecord(ESM::ESMReader &reader, uint32_t type) { if (type != ESM::REC_KEYS) return; @@ -489,13 +502,15 @@ namespace MWGui MagicSelectionDialog::MagicSelectionDialog(QuickKeysMenu* parent) : WindowModal("openmw_magicselection_dialog.layout") , mParent(parent) - , mWidth(0) - , mHeight(0) { getWidget(mCancelButton, "CancelButton"); getWidget(mMagicList, "MagicList"); mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &MagicSelectionDialog::onCancelButtonClicked); + mMagicList->setShowCostColumn(false); + mMagicList->setHighlightSelected(false); + mMagicList->eventSpellClicked += MyGUI::newDelegate(this, &MagicSelectionDialog::onModelIndexSelected); + center(); } @@ -513,210 +528,17 @@ namespace MWGui { WindowModal::open(); - while (mMagicList->getChildCount ()) - MyGUI::Gui::getInstance ().destroyWidget (mMagicList->getChildAt (0)); - - mHeight = 0; - - const int spellHeight = 18; - - MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); - MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); - MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); - MWMechanics::Spells& spells = stats.getSpells(); - - /// \todo lots of copy&pasted code from SpellWindow - - // retrieve powers & spells, sort by name - std::vector spellList; - - for (MWMechanics::Spells::TIterator it = spells.begin(); it != spells.end(); ++it) - { - spellList.push_back (it->first); - } - - const MWWorld::ESMStore &esmStore = - MWBase::Environment::get().getWorld()->getStore(); - - std::vector powers; - std::vector::iterator it = spellList.begin(); - while (it != spellList.end()) - { - const ESM::Spell* spell = esmStore.get().find(*it); - if (spell->mData.mType == ESM::Spell::ST_Power) - { - powers.push_back(*it); - it = spellList.erase(it); - } - else if (spell->mData.mType == ESM::Spell::ST_Ability - || spell->mData.mType == ESM::Spell::ST_Blight - || spell->mData.mType == ESM::Spell::ST_Curse - || spell->mData.mType == ESM::Spell::ST_Disease) - { - it = spellList.erase(it); - } - else - ++it; - } - std::sort(powers.begin(), powers.end(), sortSpells); - std::sort(spellList.begin(), spellList.end(), sortSpells); - - // retrieve usable magic items & sort - std::vector items; - for (MWWorld::ContainerStoreIterator it(store.begin()); it != store.end(); ++it) - { - std::string enchantId = it->getClass().getEnchantment(*it); - if (enchantId != "") - { - // only add items with "Cast once" or "Cast on use" - const ESM::Enchantment* enchant = - esmStore.get().find(enchantId); - - int type = enchant->mData.mType; - if (type != ESM::Enchantment::CastOnce - && type != ESM::Enchantment::WhenUsed) - continue; - - items.push_back(*it); - } - } - std::sort(items.begin(), items.end(), sortItems); - - - int height = estimateHeight(items.size() + powers.size() + spellList.size()); - bool scrollVisible = height > mMagicList->getHeight(); - mWidth = mMagicList->getWidth() - scrollVisible * 18; - - - // powers - addGroup("#{sPowers}", ""); - - for (std::vector::const_iterator it = powers.begin(); it != powers.end(); ++it) - { - const ESM::Spell* spell = esmStore.get().find(*it); - MyGUI::Button* t = mMagicList->createWidget("SpellText", - MyGUI::IntCoord(4, mHeight, mWidth-8, spellHeight), MyGUI::Align::Left | MyGUI::Align::Top); - t->setCaption(spell->mName); - t->setTextAlign(MyGUI::Align::Left); - t->setUserString("ToolTipType", "Spell"); - t->setUserString("Spell", *it); - t->eventMouseWheel += MyGUI::newDelegate(this, &MagicSelectionDialog::onMouseWheel); - t->eventMouseButtonClick += MyGUI::newDelegate(this, &MagicSelectionDialog::onSpellSelected); - - mHeight += spellHeight; - } - - // other spells - addGroup("#{sSpells}", ""); - for (std::vector::const_iterator it = spellList.begin(); it != spellList.end(); ++it) - { - const ESM::Spell* spell = esmStore.get().find(*it); - MyGUI::Button* t = mMagicList->createWidget("SpellText", - MyGUI::IntCoord(4, mHeight, mWidth-8, spellHeight), MyGUI::Align::Left | MyGUI::Align::Top); - t->setCaption(spell->mName); - t->setTextAlign(MyGUI::Align::Left); - t->setUserString("ToolTipType", "Spell"); - t->setUserString("Spell", *it); - t->eventMouseWheel += MyGUI::newDelegate(this, &MagicSelectionDialog::onMouseWheel); - t->eventMouseButtonClick += MyGUI::newDelegate(this, &MagicSelectionDialog::onSpellSelected); - - mHeight += spellHeight; - } - - - // enchanted items - addGroup("#{sMagicItem}", ""); - - for (std::vector::const_iterator it = items.begin(); it != items.end(); ++it) - { - MWWorld::Ptr item = *it; - - // check if the item is currently equipped (will display in a different color) - bool equipped = false; - for (int i=0; i < MWWorld::InventoryStore::Slots; ++i) - { - if (store.getSlot(i) != store.end() && *store.getSlot(i) == item) - { - equipped = true; - break; - } - } - - MyGUI::Button* t = mMagicList->createWidget(equipped ? "SpellText" : "SpellTextUnequipped", - MyGUI::IntCoord(4, mHeight, mWidth-8, spellHeight), MyGUI::Align::Left | MyGUI::Align::Top); - t->setCaption(item.getClass().getName(item)); - t->setTextAlign(MyGUI::Align::Left); - t->setUserData(item); - t->setUserString("ToolTipType", "ItemPtr"); - t->eventMouseButtonClick += MyGUI::newDelegate(this, &MagicSelectionDialog::onEnchantedItemSelected); - t->eventMouseWheel += MyGUI::newDelegate(this, &MagicSelectionDialog::onMouseWheel); - - mHeight += spellHeight; - } - - - mMagicList->setCanvasSize (mWidth, std::max(mMagicList->getHeight(), mHeight)); - - } - - void MagicSelectionDialog::addGroup(const std::string &label, const std::string& label2) - { - if (mMagicList->getChildCount() > 0) - { - MyGUI::ImageBox* separator = mMagicList->createWidget("MW_HLine", - MyGUI::IntCoord(4, mHeight, mWidth-8, 18), - MyGUI::Align::Left | MyGUI::Align::Top); - separator->setNeedMouseFocus(false); - mHeight += 18; - } - - MyGUI::TextBox* groupWidget = mMagicList->createWidget("SandBrightText", - MyGUI::IntCoord(0, mHeight, mWidth, 24), - MyGUI::Align::Left | MyGUI::Align::Top | MyGUI::Align::HStretch); - groupWidget->setCaptionWithReplacing(label); - groupWidget->setTextAlign(MyGUI::Align::Left); - groupWidget->setNeedMouseFocus(false); - - if (label2 != "") - { - MyGUI::TextBox* groupWidget2 = mMagicList->createWidget("SandBrightText", - MyGUI::IntCoord(0, mHeight, mWidth-4, 24), - MyGUI::Align::Left | MyGUI::Align::Top); - groupWidget2->setCaptionWithReplacing(label2); - groupWidget2->setTextAlign(MyGUI::Align::Right); - groupWidget2->setNeedMouseFocus(false); - } - - mHeight += 24; + mMagicList->setModel(new SpellModel(MWBase::Environment::get().getWorld()->getPlayerPtr())); + mMagicList->update(); } - - void MagicSelectionDialog::onMouseWheel(MyGUI::Widget* _sender, int _rel) + void MagicSelectionDialog::onModelIndexSelected(SpellModel::ModelIndex index) { - if (mMagicList->getViewOffset().top + _rel*0.3 > 0) - mMagicList->setViewOffset(MyGUI::IntPoint(0, 0)); + const Spell& spell = mMagicList->getModel()->getItem(index); + if (spell.mType == Spell::Type_EnchantedItem) + mParent->onAssignMagicItem(spell.mItem); else - mMagicList->setViewOffset(MyGUI::IntPoint(0, mMagicList->getViewOffset().top + _rel*0.3)); - } - - void MagicSelectionDialog::onEnchantedItemSelected(MyGUI::Widget* _sender) - { - MWWorld::Ptr item = *_sender->getUserData(); - - mParent->onAssignMagicItem (item); - } - - void MagicSelectionDialog::onSpellSelected(MyGUI::Widget* _sender) - { - mParent->onAssignMagic (_sender->getUserString("Spell")); - } - - int MagicSelectionDialog::estimateHeight(int numSpells) const - { - int height = 0; - height += 24 * 3 + 18 * 2; // group headings - height += numSpells * 18; - return height; + mParent->onAssignMagic(spell.mId); } } diff --git a/apps/openmw/mwgui/quickkeysmenu.hpp b/apps/openmw/mwgui/quickkeysmenu.hpp index dc088d3c9..00afa4561 100644 --- a/apps/openmw/mwgui/quickkeysmenu.hpp +++ b/apps/openmw/mwgui/quickkeysmenu.hpp @@ -5,6 +5,8 @@ #include "windowbase.hpp" +#include "spellmodel.hpp" + namespace MWGui { @@ -12,6 +14,7 @@ namespace MWGui class ItemSelectionDialog; class MagicSelectionDialog; class ItemWidget; + class SpellView; class QuickKeysMenu : public WindowBase { @@ -44,7 +47,7 @@ namespace MWGui void write (ESM::ESMWriter& writer); - void readRecord (ESM::ESMReader& reader, int32_t type); + void readRecord (ESM::ESMReader& reader, uint32_t type); void clear(); @@ -94,21 +97,12 @@ namespace MWGui private: MyGUI::Button* mCancelButton; - MyGUI::ScrollView* mMagicList; - - int mWidth; - int mHeight; + SpellView* mMagicList; QuickKeysMenu* mParent; void onCancelButtonClicked (MyGUI::Widget* sender); - void onMouseWheel(MyGUI::Widget* _sender, int _rel); - void onEnchantedItemSelected(MyGUI::Widget* _sender); - void onSpellSelected(MyGUI::Widget* _sender); - int estimateHeight(int numSpells) const; - - - void addGroup(const std::string& label, const std::string& label2); + void onModelIndexSelected(SpellModel::ModelIndex index); }; } diff --git a/apps/openmw/mwgui/race.cpp b/apps/openmw/mwgui/race.cpp index 499c1e191..b03bf758a 100644 --- a/apps/openmw/mwgui/race.cpp +++ b/apps/openmw/mwgui/race.cpp @@ -1,11 +1,17 @@ #include "race.hpp" -#include +#include +#include +#include +#include + #include +#include "../mwworld/esmstore.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwrender/characterpreview.hpp" #include "tooltips.hpp" @@ -20,6 +26,12 @@ namespace else return index; } + + bool sortRaces(const std::pair& left, const std::pair& right) + { + return left.second.compare(right.second) < 0; + } + } namespace MWGui @@ -32,7 +44,6 @@ namespace MWGui , mHairIndex(0) , mCurrentAngle(0) , mPreviewDirty(true) - , mPreview(NULL) { // Centre dialog center(); @@ -42,12 +53,11 @@ namespace MWGui getWidget(mHeadRotate, "HeadRotate"); - // Mouse wheel step is hardcoded to 50 in MyGUI 3.2 ("FIXME"). - // Give other steps the same value to accomodate. mHeadRotate->setScrollRange(1000); mHeadRotate->setScrollPosition(500); mHeadRotate->setScrollViewPage(50); mHeadRotate->setScrollPage(50); + mHeadRotate->setScrollWheelPage(50); mHeadRotate->eventScrollChangePosition += MyGUI::newDelegate(this, &RaceDialog::onHeadRotate); // Set up next/previous buttons @@ -115,25 +125,41 @@ namespace MWGui updateSkills(); updateSpellPowers(); - mPreview = new MWRender::RaceSelectionPreview(); + mPreview.reset(NULL); + + mPreviewImage->setImageTexture(""); + + const std::string textureName = "CharacterHeadPreview"; + MyGUI::RenderManager::getInstance().destroyTexture(MyGUI::RenderManager::getInstance().getTexture(textureName)); + + mPreview.reset(new MWRender::RaceSelectionPreview()); mPreview->setup(); - mPreview->update (0); + mPreview->update (mCurrentAngle); const ESM::NPC proto = mPreview->getPrototype(); setRaceId(proto.mRace); recountParts(); - std::string index = proto.mHead.substr(proto.mHead.size() - 2, 2); - mFaceIndex = boost::lexical_cast(index) - 1; + for (unsigned int i=0; i(index) - 1; + for (unsigned int i=0; isetImageTexture ("CharacterHeadPreview"); + mPreviewImage->setImageTexture (textureName); mPreviewDirty = true; - } + size_t initialPos = mHeadRotate->getScrollRange()/2+mHeadRotate->getScrollRange()/10; + mHeadRotate->setScrollPosition(initialPos); + onHeadRotate(mHeadRotate, initialPos); + } void RaceDialog::setRaceId(const std::string &raceId) { @@ -145,8 +171,6 @@ namespace MWGui if (Misc::StringUtils::ciEqual(*mRaceList->getItemDataAt(i), raceId)) { mRaceList->setIndexSelected(i); - MyGUI::Button* okButton; - getWidget(okButton, "OKButton"); break; } } @@ -157,8 +181,10 @@ namespace MWGui void RaceDialog::close() { - delete mPreview; - mPreview = 0; + mPreviewImage->setImageTexture(""); + const std::string textureName = "CharacterHeadPreview"; + MyGUI::RenderManager::getInstance().destroyTexture(MyGUI::RenderManager::getInstance().getTexture(textureName)); + mPreview.reset(NULL); } // widget controls @@ -178,10 +204,9 @@ namespace MWGui void RaceDialog::onHeadRotate(MyGUI::ScrollBar* scroll, size_t _position) { float angle = (float(_position) / (scroll->getScrollRange()-1) - 0.5) * 3.14 * 2; - float diff = angle - mCurrentAngle; - mPreview->update (diff); + mPreview->update (angle); mPreviewDirty = true; - mCurrentAngle += diff; + mCurrentAngle = angle; } void RaceDialog::onSelectPreviousGender(MyGUI::Widget*) @@ -229,8 +254,6 @@ namespace MWGui if (_index == MyGUI::ITEM_NONE) return; - MyGUI::Button* okButton; - getWidget(okButton, "OKButton"); const std::string *raceId = mRaceList->getItemDataAt(_index); if (Misc::StringUtils::ciEqual(mCurrentRaceId, *raceId)) return; @@ -300,12 +323,24 @@ namespace MWGui record.mHead = mAvailableHeads[mFaceIndex]; record.mHair = mAvailableHairs[mHairIndex]; - mPreview->setPrototype(record); + try + { + mPreview->setPrototype(record); + } + catch (std::exception& e) + { + std::cerr << "Error creating preview: " << e.what() << std::endl; + } + mPreviewDirty = true; } void RaceDialog::doRenderUpdate() { + if (!mPreview.get()) + return; + + mPreview->onFrame(); if (mPreviewDirty) { mPreview->render(); @@ -320,8 +355,7 @@ namespace MWGui const MWWorld::Store &races = MWBase::Environment::get().getWorld()->getStore().get(); - - int index = 0; + std::vector > items; // ID, name MWWorld::Store::iterator it = races.begin(); for (; it != races.end(); ++it) { @@ -329,8 +363,15 @@ namespace MWGui if (!playable) // Only display playable races continue; - mRaceList->addItem(it->mName, it->mId); - if (Misc::StringUtils::ciEqual(it->mId, mCurrentRaceId)) + items.push_back(std::make_pair(it->mId, it->mName)); + } + std::sort(items.begin(), items.end(), sortRaces); + + int index = 0; + for (std::vector >::const_iterator it = items.begin(); it != items.end(); ++it) + { + mRaceList->addItem(it->second, it->first); + if (Misc::StringUtils::ciEqual(it->first, mCurrentRaceId)) mRaceList->setIndexSelected(index); ++index; } @@ -361,7 +402,7 @@ namespace MWGui continue; skillWidget = mSkillList->createWidget("MW_StatNameValue", coord1, MyGUI::Align::Default, - std::string("Skill") + boost::lexical_cast(i)); + std::string("Skill") + MyGUI::utility::toString(i)); skillWidget->setSkillNumber(skillId); skillWidget->setSkillValue(Widgets::MWSkill::SkillValue(race->mData.mBonus[i].mBonus)); ToolTips::createSkillToolTip(skillWidget, skillId); @@ -384,7 +425,6 @@ namespace MWGui if (mCurrentRaceId.empty()) return; - Widgets::MWSpellPtr spellPowerWidget; const int lineHeight = 18; MyGUI::IntCoord coord(0, 0, mSpellPowerList->getWidth(), 18); @@ -396,7 +436,7 @@ namespace MWGui for (int i = 0; it != end; ++it) { const std::string &spellpower = *it; - spellPowerWidget = mSpellPowerList->createWidget("MW_StatName", coord, MyGUI::Align::Default, std::string("SpellPower") + boost::lexical_cast(i)); + Widgets::MWSpellPtr spellPowerWidget = mSpellPowerList->createWidget("MW_StatName", coord, MyGUI::Align::Default, std::string("SpellPower") + MyGUI::utility::toString(i)); spellPowerWidget->setSpellId(spellpower); spellPowerWidget->setUserString("ToolTipType", "Spell"); spellPowerWidget->setUserString("Spell", spellpower); @@ -407,4 +447,9 @@ namespace MWGui ++i; } } + + const ESM::NPC& RaceDialog::getResult() const + { + return mPreview->getPrototype(); + } } diff --git a/apps/openmw/mwgui/race.hpp b/apps/openmw/mwgui/race.hpp index 340dcfa27..be16af5d1 100644 --- a/apps/openmw/mwgui/race.hpp +++ b/apps/openmw/mwgui/race.hpp @@ -1,8 +1,6 @@ #ifndef MWGUI_RACE_H #define MWGUI_RACE_H -#include "../mwrender/characterpreview.hpp" - #include "windowbase.hpp" @@ -11,10 +9,15 @@ namespace MWGui class WindowManager; } -/* - This file contains the dialog for choosing a race. - Layout is defined by resources/mygui/openmw_chargen_race.layout. - */ +namespace MWRender +{ + class RaceSelectionPreview; +} + +namespace ESM +{ + struct NPC; +} namespace MWGui { @@ -29,7 +32,7 @@ namespace MWGui GM_Female }; - const ESM::NPC &getResult() const { return mPreview->getPrototype(); } + const ESM::NPC &getResult() const; const std::string &getRaceId() const { return mCurrentRaceId; } Gender getGender() const { return mGenderIndex == 0 ? GM_Male : GM_Female; } // getFace() @@ -52,6 +55,11 @@ namespace MWGui */ EventHandle_Void eventBack; + /** Event : Dialog finished, OK button clicked.\n + signature : void method()\n + */ + EventHandle_WindowBase eventDone; + void doRenderUpdate(); protected: @@ -100,7 +108,7 @@ namespace MWGui float mCurrentAngle; - MWRender::RaceSelectionPreview* mPreview; + std::auto_ptr mPreview; bool mPreviewDirty; }; diff --git a/apps/openmw/mwgui/recharge.cpp b/apps/openmw/mwgui/recharge.cpp index 9c43b1416..2c854a8f5 100644 --- a/apps/openmw/mwgui/recharge.cpp +++ b/apps/openmw/mwgui/recharge.cpp @@ -1,14 +1,19 @@ #include "recharge.hpp" -#include #include +#include +#include + +#include + #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/class.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/npcstats.hpp" @@ -60,7 +65,7 @@ void Recharge::updateView() std::string soul = gem.getCellRef().getSoul(); const ESM::Creature *creature = MWBase::Environment::get().getWorld()->getStore().get().find(soul); - mChargeLabel->setCaptionWithReplacing("#{sCharges} " + boost::lexical_cast(creature->mData.mSoul)); + mChargeLabel->setCaptionWithReplacing("#{sCharges} " + MyGUI::utility::toString(creature->mData.mSoul)); bool toolBoxVisible = (gem.getRefData().getCount() != 0); mGemBox->setVisible(toolBoxVisible); @@ -119,7 +124,11 @@ void Recharge::updateView() currentY += 32 + 4; } + + // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden + mView->setVisibleVScroll(false); mView->setCanvasSize (MyGUI::IntSize(mView->getWidth(), std::max(mView->getHeight(), currentY))); + mView->setVisibleVScroll(true); } void Recharge::onCancel(MyGUI::Widget *sender) @@ -177,6 +186,10 @@ void Recharge::onItemClicked(MyGUI::Widget *sender) std::string message = MWBase::Environment::get().getWorld()->getStore().get().find("sNotifyMessage51")->getString(); message = boost::str(boost::format(message) % gem.getClass().getName(gem)); MWBase::Environment::get().getWindowManager()->messageBox(message); + + // special case: readd Azura's Star + if (Misc::StringUtils::ciEqual(gem.get()->mBase->mId, "Misc_SoulGem_Azura")) + player.getClass().getContainerStore(player).add("Misc_SoulGem_Azura", 1, player); } updateView(); diff --git a/apps/openmw/mwgui/recharge.hpp b/apps/openmw/mwgui/recharge.hpp index 17d700649..3e8e1269e 100644 --- a/apps/openmw/mwgui/recharge.hpp +++ b/apps/openmw/mwgui/recharge.hpp @@ -3,7 +3,10 @@ #include "windowbase.hpp" -#include "../mwworld/ptr.hpp" +namespace MWWorld +{ + class Ptr; +} namespace MWGui { diff --git a/apps/openmw/mwgui/repair.cpp b/apps/openmw/mwgui/repair.cpp index 986e27243..c3c971400 100644 --- a/apps/openmw/mwgui/repair.cpp +++ b/apps/openmw/mwgui/repair.cpp @@ -2,7 +2,8 @@ #include -#include +#include +#include #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" @@ -65,7 +66,7 @@ void Repair::updateRepairView() std::stringstream qualityStr; qualityStr << std::setprecision(3) << quality; - mUsesLabel->setCaptionWithReplacing("#{sUses} " + boost::lexical_cast(uses)); + mUsesLabel->setCaptionWithReplacing("#{sUses} " + MyGUI::utility::toString(uses)); mQualityLabel->setCaptionWithReplacing("#{sQuality} " + qualityStr.str()); bool toolBoxVisible = (mRepair.getTool().getRefData().getCount() != 0); @@ -126,7 +127,10 @@ void Repair::updateRepairView() currentY += 32 + 4; } } + // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden + mRepairView->setVisibleVScroll(false); mRepairView->setCanvasSize (MyGUI::IntSize(mRepairView->getWidth(), std::max(mRepairView->getHeight(), currentY))); + mRepairView->setVisibleVScroll(true); } void Repair::onCancel(MyGUI::Widget *sender) diff --git a/apps/openmw/mwgui/review.cpp b/apps/openmw/mwgui/review.cpp index e27e40ae6..1bcd2d31a 100644 --- a/apps/openmw/mwgui/review.cpp +++ b/apps/openmw/mwgui/review.cpp @@ -1,16 +1,29 @@ #include "review.hpp" -#include +#include +#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwworld/esmstore.hpp" #include "tooltips.hpp" #undef min #undef max +namespace +{ + void adjustButtonSize(MyGUI::Button *button) + { + // adjust size of button to fit its text + MyGUI::IntSize size = button->getTextSize(); + button->setSize(size.width + 24, button->getSize().height); + } +} + namespace MWGui { @@ -27,22 +40,22 @@ namespace MWGui getWidget(mNameWidget, "NameText"); getWidget(button, "NameButton"); adjustButtonSize(button); - button->eventMouseButtonClick += MyGUI::newDelegate(this, &ReviewDialog::onNameClicked);; + button->eventMouseButtonClick += MyGUI::newDelegate(this, &ReviewDialog::onNameClicked); getWidget(mRaceWidget, "RaceText"); getWidget(button, "RaceButton"); adjustButtonSize(button); - button->eventMouseButtonClick += MyGUI::newDelegate(this, &ReviewDialog::onRaceClicked);; + button->eventMouseButtonClick += MyGUI::newDelegate(this, &ReviewDialog::onRaceClicked); getWidget(mClassWidget, "ClassText"); getWidget(button, "ClassButton"); adjustButtonSize(button); - button->eventMouseButtonClick += MyGUI::newDelegate(this, &ReviewDialog::onClassClicked);; + button->eventMouseButtonClick += MyGUI::newDelegate(this, &ReviewDialog::onClassClicked); getWidget(mBirthSignWidget, "SignText"); getWidget(button, "SignButton"); adjustButtonSize(button); - button->eventMouseButtonClick += MyGUI::newDelegate(this, &ReviewDialog::onBirthSignClicked);; + button->eventMouseButtonClick += MyGUI::newDelegate(this, &ReviewDialog::onBirthSignClicked); // Setup dynamic stats getWidget(mHealth, "Health"); @@ -62,7 +75,7 @@ namespace MWGui Widgets::MWAttributePtr attribute; for (int idx = 0; idx < ESM::Attribute::Length; ++idx) { - getWidget(attribute, std::string("Attribute") + boost::lexical_cast(idx)); + getWidget(attribute, std::string("Attribute") + MyGUI::utility::toString(idx)); mAttributeWidgets.insert(std::make_pair(static_cast(ESM::Attribute::sAttributeIds[idx]), attribute)); attribute->setAttributeId(ESM::Attribute::sAttributeIds[idx]); attribute->setAttributeValue(Widgets::MWAttribute::AttributeValue()); @@ -134,21 +147,21 @@ namespace MWGui void ReviewDialog::setHealth(const MWMechanics::DynamicStat& value) { mHealth->setValue(value.getCurrent(), value.getModified()); - std::string valStr = boost::lexical_cast(value.getCurrent()) + "/" + boost::lexical_cast(value.getModified()); + std::string valStr = MyGUI::utility::toString(value.getCurrent()) + "/" + MyGUI::utility::toString(value.getModified()); mHealth->setUserString("Caption_HealthDescription", "#{sHealthDesc}\n" + valStr); } void ReviewDialog::setMagicka(const MWMechanics::DynamicStat& value) { mMagicka->setValue(value.getCurrent(), value.getModified()); - std::string valStr = boost::lexical_cast(value.getCurrent()) + "/" + boost::lexical_cast(value.getModified()); + std::string valStr = MyGUI::utility::toString(value.getCurrent()) + "/" + MyGUI::utility::toString(value.getModified()); mMagicka->setUserString("Caption_HealthDescription", "#{sIntDesc}\n" + valStr); } void ReviewDialog::setFatigue(const MWMechanics::DynamicStat& value) { mFatigue->setValue(value.getCurrent(), value.getModified()); - std::string valStr = boost::lexical_cast(value.getCurrent()) + "/" + boost::lexical_cast(value.getModified()); + std::string valStr = MyGUI::utility::toString(value.getCurrent()) + "/" + MyGUI::utility::toString(value.getModified()); mFatigue->setUserString("Caption_HealthDescription", "#{sFatDesc}\n" + valStr); } @@ -168,7 +181,7 @@ namespace MWGui if (widget) { float modified = value.getModified(), base = value.getBase(); - std::string text = boost::lexical_cast(std::floor(modified)); + std::string text = MyGUI::utility::toString(std::floor(modified)); std::string state = "normal"; if (modified > base) state = "increased"; @@ -288,7 +301,7 @@ namespace MWGui state = "increased"; else if (modified < base) state = "decreased"; - MyGUI::TextBox* widget = addValueItem(MWBase::Environment::get().getWindowManager()->getGameSettingString(skillNameId, skillNameId), boost::lexical_cast(static_cast(modified)), state, coord1, coord2); + MyGUI::TextBox* widget = addValueItem(MWBase::Environment::get().getWindowManager()->getGameSettingString(skillNameId, skillNameId), MyGUI::utility::toString(static_cast(modified)), state, coord1, coord2); for (int i=0; i<2; ++i) { @@ -320,7 +333,10 @@ namespace MWGui if (!mMiscSkills.empty()) addSkills(mMiscSkills, "sSkillClassMisc", "Misc Skills", coord1, coord2); + // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden + mSkillView->setVisibleVScroll(false); mSkillView->setCanvasSize (mSkillView->getWidth(), std::max(mSkillView->getHeight(), coord1.top)); + mSkillView->setVisibleVScroll(true); } // widget controls diff --git a/apps/openmw/mwgui/review.hpp b/apps/openmw/mwgui/review.hpp index 5d0767d21..111d7de1d 100644 --- a/apps/openmw/mwgui/review.hpp +++ b/apps/openmw/mwgui/review.hpp @@ -1,6 +1,8 @@ #ifndef MWGUI_REVIEW_H #define MWGUI_REVIEW_H +#include +#include #include "windowbase.hpp" #include "widgets.hpp" @@ -9,11 +11,6 @@ namespace MWGui class WindowManager; } -/* -This file contains the dialog for reviewing the generated character. -Layout is defined by resources/mygui/openmw_chargen_review.layout. -*/ - namespace MWGui { class ReviewDialog : public WindowModal @@ -54,6 +51,11 @@ namespace MWGui */ EventHandle_Void eventBack; + /** Event : Dialog finished, OK button clicked.\n + signature : void method()\n + */ + EventHandle_WindowBase eventDone; + EventHandle_Int eventActivateDialog; protected: diff --git a/apps/openmw/mwgui/savegamedialog.cpp b/apps/openmw/mwgui/savegamedialog.cpp index a35415e75..d022c8e25 100644 --- a/apps/openmw/mwgui/savegamedialog.cpp +++ b/apps/openmw/mwgui/savegamedialog.cpp @@ -4,6 +4,11 @@ #include #include +#include +#include +#include +#include + #include #include @@ -12,6 +17,7 @@ #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwstate/character.hpp" @@ -37,10 +43,11 @@ namespace MWGui mOkButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SaveGameDialog::onOkButtonClicked); mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SaveGameDialog::onCancelButtonClicked); mDeleteButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SaveGameDialog::onDeleteButtonClicked); - mCharacterSelection->eventComboChangePosition += MyGUI::newDelegate(this, &SaveGameDialog::onCharacterSelected); + mCharacterSelection->eventComboAccept += MyGUI::newDelegate(this, &SaveGameDialog::onCharacterSelected); mSaveList->eventListChangePosition += MyGUI::newDelegate(this, &SaveGameDialog::onSlotSelected); mSaveList->eventListMouseItemActivate += MyGUI::newDelegate(this, &SaveGameDialog::onSlotMouseClick); mSaveList->eventListSelectAccept += MyGUI::newDelegate(this, &SaveGameDialog::onSlotActivated); + mSaveList->eventKeyButtonPressed += MyGUI::newDelegate(this, &SaveGameDialog::onKeyButtonPressed); mSaveNameEdit->eventEditSelectAccept += MyGUI::newDelegate(this, &SaveGameDialog::onEditSelectAccept); mSaveNameEdit->eventEditTextChange += MyGUI::newDelegate(this, &SaveGameDialog::onSaveNameChanged); } @@ -212,6 +219,7 @@ namespace MWGui void SaveGameDialog::accept(bool reallySure) { + // Remove for MyGUI 3.2.2 MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(NULL); if (mSaving) @@ -243,10 +251,16 @@ namespace MWGui else { assert (mCurrentCharacter && mCurrentSlot); - MWBase::Environment::get().getStateManager()->loadGame (mCurrentCharacter, mCurrentSlot); + MWBase::Environment::get().getStateManager()->loadGame (mCurrentCharacter, mCurrentSlot->mPath.string()); } } + void SaveGameDialog::onKeyButtonPressed(MyGUI::Widget* _sender, MyGUI::KeyCode key, MyGUI::Char character) + { + if (key == MyGUI::KeyCode::Delete && mCurrentSlot) + confirmDeleteSave(); + } + void SaveGameDialog::onOkButtonClicked(MyGUI::Widget *sender) { accept(); @@ -296,7 +310,7 @@ namespace MWGui mOkButton->setEnabled(pos != MyGUI::ITEM_NONE || mSaving); mDeleteButton->setEnabled(pos != MyGUI::ITEM_NONE); - if (pos == MyGUI::ITEM_NONE) + if (pos == MyGUI::ITEM_NONE || !mCurrentCharacter) { mCurrentSlot = NULL; mInfoText->setCaption(""); @@ -314,7 +328,8 @@ namespace MWGui if (i == pos) mCurrentSlot = &*it; } - assert(mCurrentSlot && "Can't find selected slot"); + if (!mCurrentSlot) + throw std::runtime_error("Can't find selected slot"); std::stringstream text; time_t time = mCurrentSlot->mTimeStamp; diff --git a/apps/openmw/mwgui/savegamedialog.hpp b/apps/openmw/mwgui/savegamedialog.hpp index 80cfad279..11470a20f 100644 --- a/apps/openmw/mwgui/savegamedialog.hpp +++ b/apps/openmw/mwgui/savegamedialog.hpp @@ -26,6 +26,7 @@ namespace MWGui private: void confirmDeleteSave(); + void onKeyButtonPressed(MyGUI::Widget* _sender, MyGUI::KeyCode key, MyGUI::Char character); void onCancelButtonClicked (MyGUI::Widget* sender); void onOkButtonClicked (MyGUI::Widget* sender); void onDeleteButtonClicked (MyGUI::Widget* sender); diff --git a/apps/openmw/mwgui/screenfader.cpp b/apps/openmw/mwgui/screenfader.cpp new file mode 100644 index 000000000..473776a82 --- /dev/null +++ b/apps/openmw/mwgui/screenfader.cpp @@ -0,0 +1,178 @@ +#include "screenfader.hpp" + +#include + +namespace MWGui +{ + + FadeOp::FadeOp(ScreenFader * fader, float time, float targetAlpha) + : mFader(fader), + mRemainingTime(time), + mTargetTime(time), + mTargetAlpha(targetAlpha), + mStartAlpha(0.f), + mRunning(false) + { + } + + bool FadeOp::isRunning() + { + return mRunning; + } + + void FadeOp::start() + { + if (mRunning) + return; + + mRemainingTime = mTargetTime; + mStartAlpha = mFader->getCurrentAlpha(); + mRunning = true; + } + + void FadeOp::update(float dt) + { + if (!mRunning) + return; + + if (mRemainingTime <= 0 || mStartAlpha == mTargetAlpha) + { + finish(); + return; + } + + float currentAlpha = mFader->getCurrentAlpha(); + if (mStartAlpha > mTargetAlpha) + { + currentAlpha -= dt/mTargetTime * (mStartAlpha-mTargetAlpha); + if (currentAlpha < mTargetAlpha) + currentAlpha = mTargetAlpha; + } + else + { + currentAlpha += dt/mTargetTime * (mTargetAlpha-mStartAlpha); + if (currentAlpha > mTargetAlpha) + currentAlpha = mTargetAlpha; + } + + mFader->notifyAlphaChanged(currentAlpha); + + mRemainingTime -= dt; + } + + void FadeOp::finish() + { + mRunning = false; + mFader->notifyOperationFinished(); + } + + ScreenFader::ScreenFader(const std::string & texturePath) + : WindowBase("openmw_screen_fader.layout") + , mCurrentAlpha(0.f) + , mFactor(1.f) + , mRepeat(false) + { + mMainWidget->setSize(MyGUI::RenderManager::getInstance().getViewSize()); + setTexture(texturePath); + setVisible(false); + } + + void ScreenFader::setTexture(const std::string & texturePath) + { + mMainWidget->setProperty("ImageTexture", texturePath); + } + + void ScreenFader::update(float dt) + { + if (!mQueue.empty()) + { + if (!mQueue.front()->isRunning()) + mQueue.front()->start(); + mQueue.front()->update(dt); + } + } + + void ScreenFader::applyAlpha() + { + setVisible(true); + mMainWidget->setAlpha(1.f-((1.f-mCurrentAlpha) * mFactor)); + } + + void ScreenFader::fadeIn(float time) + { + queue(time, 1.f); + } + + void ScreenFader::fadeOut(const float time) + { + queue(time, 0.f); + } + + void ScreenFader::fadeTo(const int percent, const float time) + { + queue(time, percent/100.f); + } + + void ScreenFader::setFactor(float factor) + { + mFactor = factor; + applyAlpha(); + } + + void ScreenFader::setRepeat(bool repeat) + { + mRepeat = repeat; + } + + void ScreenFader::queue(float time, float targetAlpha) + { + if (time < 0.f) + return; + + if (time == 0.f) + { + mCurrentAlpha = targetAlpha; + applyAlpha(); + return; + } + + mQueue.push_back(FadeOp::Ptr(new FadeOp(this, time, targetAlpha))); + } + + bool ScreenFader::isEmpty() + { + return mQueue.empty(); + } + + void ScreenFader::clearQueue() + { + mQueue.clear(); + } + + void ScreenFader::notifyAlphaChanged(float alpha) + { + if (mCurrentAlpha == alpha) + return; + + mCurrentAlpha = alpha; + + if (1.f-((1.f-mCurrentAlpha) * mFactor) == 0.f) + mMainWidget->setVisible(false); + else + applyAlpha(); + } + + void ScreenFader::notifyOperationFinished() + { + FadeOp::Ptr op = mQueue.front(); + mQueue.pop_front(); + + if (mRepeat) + mQueue.push_back(op); + } + + float ScreenFader::getCurrentAlpha() + { + return mCurrentAlpha; + } +} diff --git a/apps/openmw/mwgui/screenfader.hpp b/apps/openmw/mwgui/screenfader.hpp new file mode 100644 index 000000000..402554555 --- /dev/null +++ b/apps/openmw/mwgui/screenfader.hpp @@ -0,0 +1,71 @@ +#ifndef OPENMW_MWGUI_SCREENFADER_H +#define OPENMW_MWGUI_SCREENFADER_H + +#include + +#include + +#include "windowbase.hpp" + +namespace MWGui +{ + class ScreenFader; + + class FadeOp + { + public: + typedef boost::shared_ptr Ptr; + + FadeOp(ScreenFader * fader, float time, float targetAlpha); + + bool isRunning(); + + void start(); + void update(float dt); + void finish(); + + private: + ScreenFader * mFader; + float mRemainingTime; + float mTargetTime; + float mTargetAlpha; + float mStartAlpha; + bool mRunning; + }; + + class ScreenFader : public WindowBase + { + public: + ScreenFader(const std::string & texturePath); + + void setTexture(const std::string & texturePath); + + void update(float dt); + + void fadeIn(const float time); + void fadeOut(const float time); + void fadeTo(const int percent, const float time); + + void setFactor (float factor); + void setRepeat(bool repeat); + + void queue(float time, float targetAlpha); + bool isEmpty(); + void clearQueue(); + + void notifyAlphaChanged(float alpha); + void notifyOperationFinished(); + float getCurrentAlpha(); + + private: + void applyAlpha(); + + float mCurrentAlpha; + float mFactor; + + bool mRepeat; // repeat queued operations without removing them + std::deque mQueue; + }; +} + +#endif diff --git a/apps/openmw/mwgui/scrollwindow.cpp b/apps/openmw/mwgui/scrollwindow.cpp index 3d0751114..d61693d39 100644 --- a/apps/openmw/mwgui/scrollwindow.cpp +++ b/apps/openmw/mwgui/scrollwindow.cpp @@ -1,6 +1,9 @@ #include "scrollwindow.hpp" +#include + #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -13,7 +16,7 @@ namespace { - void adjustButton (MWGui::ImageButton* button) + void adjustButton (Gui::ImageButton* button) { MyGUI::IntSize diff = button->getSize() - button->getRequestedSize(); button->setSize(button->getRequestedSize()); @@ -54,13 +57,17 @@ namespace MWGui MWWorld::LiveCellRef *ref = mScroll.get(); - BookTextParser parser; - MyGUI::IntSize size = parser.parseScroll(ref->mBase->mText, mTextView, 390); + Formatting::BookFormatter formatter; + formatter.markupToWidget(mTextView, ref->mBase->mText, 390, mTextView->getHeight()); + MyGUI::IntSize size = mTextView->getChildAt(0)->getSize(); + // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden + mTextView->setVisibleVScroll(false); if (size.height > mTextView->getSize().height) mTextView->setCanvasSize(MyGUI::IntSize(410, size.height)); else mTextView->setCanvasSize(410, mTextView->getSize().height); + mTextView->setVisibleVScroll(true); mTextView->setViewOffset(MyGUI::IntPoint(0,0)); diff --git a/apps/openmw/mwgui/scrollwindow.hpp b/apps/openmw/mwgui/scrollwindow.hpp index 17e56f334..e1f86529a 100644 --- a/apps/openmw/mwgui/scrollwindow.hpp +++ b/apps/openmw/mwgui/scrollwindow.hpp @@ -2,10 +2,14 @@ #define MWGUI_SCROLLWINDOW_H #include "windowbase.hpp" -#include "imagebutton.hpp" #include "../mwworld/ptr.hpp" +namespace Gui +{ + class ImageButton; +} + namespace MWGui { class ScrollWindow : public WindowBase @@ -23,8 +27,8 @@ namespace MWGui void onTakeButtonClicked (MyGUI::Widget* _sender); private: - MWGui::ImageButton* mCloseButton; - MWGui::ImageButton* mTakeButton; + Gui::ImageButton* mCloseButton; + Gui::ImageButton* mTakeButton; MyGUI::ScrollView* mTextView; MWWorld::Ptr mScroll; diff --git a/apps/openmw/mwgui/settingswindow.cpp b/apps/openmw/mwgui/settingswindow.cpp index 09d2fc19c..301e9de7e 100644 --- a/apps/openmw/mwgui/settingswindow.cpp +++ b/apps/openmw/mwgui/settingswindow.cpp @@ -1,14 +1,23 @@ #include "settingswindow.hpp" #include -#include -#include +#include +#include +#include +#include +#include +#include + #include #include #include +#include +#include +#include + #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/soundmanager.hpp" @@ -46,8 +55,8 @@ namespace assert (split.size() >= 2); boost::trim(split[0]); boost::trim(split[1]); - x = boost::lexical_cast (split[0]); - y = boost::lexical_cast (split[1]); + x = MyGUI::utility::parseInt (split[0]); + y = MyGUI::utility::parseInt (split[1]); } bool sortResolutions (std::pair left, std::pair right) @@ -65,7 +74,7 @@ namespace // special case: 8 : 5 is usually referred to as 16:10 if (xaspect == 8 && yaspect == 5) return "16 : 10"; - return boost::lexical_cast(xaspect) + " : " + boost::lexical_cast(yaspect); + return MyGUI::utility::toString(xaspect) + " : " + MyGUI::utility::toString(yaspect); } std::string hlslGlsl () @@ -103,9 +112,9 @@ namespace min = 0.f; max = 1.f; if (!widget->getUserString(settingMin).empty()) - min = boost::lexical_cast(widget->getUserString(settingMin)); + min = MyGUI::utility::parseFloat(widget->getUserString(settingMin)); if (!widget->getUserString(settingMax).empty()) - max = boost::lexical_cast(widget->getUserString(settingMax)); + max = MyGUI::utility::parseFloat(widget->getUserString(settingMax)); } } @@ -136,6 +145,7 @@ namespace MWGui float min,max; getSettingMinMax(scroll, min, max); float value = Settings::Manager::getFloat(getSettingName(current), getSettingCategory(current)); + value = std::max(min, std::min(value, max)); value = (value-min)/(max-min); scroll->setScrollPosition( value * (scroll->getScrollRange()-1)); @@ -153,7 +163,8 @@ namespace MWGui } SettingsWindow::SettingsWindow() : - WindowBase("openmw_settings_window.layout") + WindowBase("openmw_settings_window.layout"), + mKeyboardMode(true) { configureWidgets(mMainWidget); @@ -163,6 +174,7 @@ namespace MWGui getWidget(mResolutionList, "ResolutionList"); getWidget(mFullscreenButton, "FullscreenButton"); getWidget(mVSyncButton, "VSyncButton"); + getWidget(mWindowBorderButton, "WindowBorderButton"); getWidget(mFPSButton, "FPSButton"); getWidget(mFOVSlider, "FOVSlider"); getWidget(mAnisotropySlider, "AnisotropySlider"); @@ -177,6 +189,24 @@ namespace MWGui getWidget(mResetControlsButton, "ResetControlsButton"); getWidget(mRefractionButton, "RefractionButton"); getWidget(mDifficultySlider, "DifficultySlider"); + getWidget(mKeyboardSwitch, "KeyboardButton"); + getWidget(mControllerSwitch, "ControllerButton"); + +#ifndef WIN32 + // hide gamma controls since it currently does not work under Linux + MyGUI::ScrollBar *gammaSlider; + getWidget(gammaSlider, "GammaSlider"); + gammaSlider->setVisible(false); + MyGUI::TextBox *textBox; + getWidget(textBox, "GammaText"); + textBox->setVisible(false); + getWidget(textBox, "GammaTextDark"); + textBox->setVisible(false); + getWidget(textBox, "GammaTextLight"); + textBox->setVisible(false); +#endif + + mMainWidget->castType()->eventWindowChangeCoord += MyGUI::newDelegate(this, &SettingsWindow::onWindowResize); mOkButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onOkButtonClicked); mShaderModeButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onShaderModeToggled); @@ -186,6 +216,9 @@ namespace MWGui mShadowsTextureSize->eventComboChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onShadowTextureSizeChanged); + mKeyboardSwitch->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onKeyboardSwitchClicked); + mControllerSwitch->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onControllerSwitchClicked); + center(); mResetControlsButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onResetDefaultBindings); @@ -204,7 +237,7 @@ namespace MWGui for (std::vector < std::pair >::const_iterator it=resolutions.begin(); it!=resolutions.end(); ++it) { - std::string str = boost::lexical_cast(it->first) + " x " + boost::lexical_cast(it->second) + std::string str = MyGUI::utility::toString(it->first) + " x " + MyGUI::utility::toString(it->second) + " (" + getAspect(it->first,it->second) + ")"; if (mResolutionList->findItemIndexWith(str) == MyGUI::ITEM_NONE) @@ -213,7 +246,7 @@ namespace MWGui std::string tf = Settings::Manager::getString("texture filtering", "General"); mTextureFilteringButton->setCaption(textureFilteringToStr(tf)); - mAnisotropyLabel->setCaption("Anisotropy (" + boost::lexical_cast(Settings::Manager::getInt("anisotropy", "General")) + ")"); + mAnisotropyLabel->setCaption("Anisotropy (" + MyGUI::utility::toString(Settings::Manager::getInt("anisotropy", "General")) + ")"); mShadowsTextureSize->setCaption (Settings::Manager::getString ("texture size", "Shadows")); @@ -229,11 +262,17 @@ namespace MWGui MyGUI::TextBox* fovText; getWidget(fovText, "FovText"); - fovText->setCaption("Field of View (" + boost::lexical_cast(int(Settings::Manager::getInt("field of view", "General"))) + ")"); + fovText->setCaption("Field of View (" + MyGUI::utility::toString(int(Settings::Manager::getInt("field of view", "General"))) + ")"); MyGUI::TextBox* diffText; getWidget(diffText, "DifficultyText"); - diffText->setCaptionWithReplacing("#{sDifficulty} (" + boost::lexical_cast(int(Settings::Manager::getInt("difficulty", "Game"))) + ")"); + + diffText->setCaptionWithReplacing("#{sDifficulty} (" + MyGUI::utility::toString(int(Settings::Manager::getInt("difficulty", "Game"))) + ")"); + + mWindowBorderButton->setEnabled(!Settings::Manager::getBool("fullscreen", "Video")); + + mKeyboardSwitch->setStateSelected(true); + mControllerSwitch->setStateSelected(false); } void SettingsWindow::onOkButtonClicked(MyGUI::Widget* _sender) @@ -293,15 +332,6 @@ namespace MWGui newState = true; } - if (_sender == mVSyncButton) - { - // Ogre::Window::setVSyncEnabled is bugged in 1.8 -#if OGRE_VERSION < (1 << 16 | 9 << 8 | 0) - MWBase::Environment::get().getWindowManager()-> - messageBox("VSync will be applied after a restart", std::vector()); -#endif - } - if (_sender == mShadersButton) { if (newState == false) @@ -358,6 +388,8 @@ namespace MWGui _sender->castType()->setCaption(off); return; } + + mWindowBorderButton->setEnabled(!newState); } if (getSettingType(_sender) == checkButtonType) @@ -370,10 +402,9 @@ namespace MWGui void SettingsWindow::onShaderModeToggled(MyGUI::Widget* _sender) { - std::string val = static_cast(_sender)->getCaption(); - val = hlslGlsl(); + std::string val = hlslGlsl(); - static_cast(_sender)->setCaption(val); + _sender->castType()->setCaption(val); Settings::Manager::setString("shader mode", "General", val); @@ -411,13 +442,13 @@ namespace MWGui { MyGUI::TextBox* fovText; getWidget(fovText, "FovText"); - fovText->setCaption("Field of View (" + boost::lexical_cast(int(value)) + ")"); + fovText->setCaption("Field of View (" + MyGUI::utility::toString(int(value)) + ")"); } if (scroller == mDifficultySlider) { MyGUI::TextBox* diffText; getWidget(diffText, "DifficultyText"); - diffText->setCaptionWithReplacing("#{sDifficulty} (" + boost::lexical_cast(int(value)) + ")"); + diffText->setCaptionWithReplacing("#{sDifficulty} (" + MyGUI::utility::toString(int(value)) + ")"); } } else @@ -425,7 +456,7 @@ namespace MWGui Settings::Manager::setInt(getSettingName(scroller), getSettingCategory(scroller), pos); if (scroller == mAnisotropySlider) { - mAnisotropyLabel->setCaption("Anisotropy (" + boost::lexical_cast(pos) + ")"); + mAnisotropyLabel->setCaption("Anisotropy (" + MyGUI::utility::toString(pos) + ")"); } } apply(); @@ -441,14 +472,37 @@ namespace MWGui MWBase::Environment::get().getInputManager()->processChangedSettings(changed); } + void SettingsWindow::onKeyboardSwitchClicked(MyGUI::Widget* _sender) + { + if(mKeyboardMode) + return; + mKeyboardMode = true; + mKeyboardSwitch->setStateSelected(true); + mControllerSwitch->setStateSelected(false); + updateControlsBox(); + } + + void SettingsWindow::onControllerSwitchClicked(MyGUI::Widget* _sender) + { + if(!mKeyboardMode) + return; + mKeyboardMode = false; + mKeyboardSwitch->setStateSelected(false); + mControllerSwitch->setStateSelected(true); + updateControlsBox(); + } + void SettingsWindow::updateControlsBox() { while (mControlsBox->getChildCount()) MyGUI::Gui::getInstance().destroyWidget(mControlsBox->getChildAt(0)); - MWBase::Environment::get().getWindowManager ()->removeStaticMessageBox(); - - std::vector actions = MWBase::Environment::get().getInputManager()->getActionSorting (); + MWBase::Environment::get().getWindowManager()->removeStaticMessageBox(); + std::vector actions; + if(mKeyboardMode) + actions = MWBase::Environment::get().getInputManager()->getActionKeySorting(); + else + actions = MWBase::Environment::get().getInputManager()->getActionControllerSorting(); const int h = 18; const int w = mControlsBox->getWidth() - 28; @@ -459,33 +513,45 @@ namespace MWGui if (desc == "") continue; - std::string binding = MWBase::Environment::get().getInputManager()->getActionBindingName (*it); + std::string binding; + if(mKeyboardMode) + binding = MWBase::Environment::get().getInputManager()->getActionKeyBindingName(*it); + else + binding = MWBase::Environment::get().getInputManager()->getActionControllerBindingName(*it); - MyGUI::TextBox* leftText = mControlsBox->createWidget("SandText", MyGUI::IntCoord(0,curH,w,h), MyGUI::Align::Default); + Gui::SharedStateButton* leftText = mControlsBox->createWidget("SandTextButton", MyGUI::IntCoord(0,curH,w,h), MyGUI::Align::Default); leftText->setCaptionWithReplacing(desc); - MyGUI::Button* rightText = mControlsBox->createWidget("SandTextButton", MyGUI::IntCoord(0,curH,w,h), MyGUI::Align::Default); + Gui::SharedStateButton* rightText = mControlsBox->createWidget("SandTextButton", MyGUI::IntCoord(0,curH,w,h), MyGUI::Align::Default); rightText->setCaptionWithReplacing(binding); rightText->setTextAlign (MyGUI::Align::Right); rightText->setUserData(*it); // save the action id for callbacks rightText->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onRebindAction); rightText->eventMouseWheel += MyGUI::newDelegate(this, &SettingsWindow::onInputTabMouseWheel); curH += h; + + Gui::ButtonGroup group; + group.push_back(leftText); + group.push_back(rightText); + Gui::SharedStateButton::createButtonGroup(group); } + // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden + mControlsBox->setVisibleVScroll(false); mControlsBox->setCanvasSize (mControlsBox->getWidth(), std::max(curH, mControlsBox->getHeight())); + mControlsBox->setVisibleVScroll(true); } void SettingsWindow::onRebindAction(MyGUI::Widget* _sender) { int actionId = *_sender->getUserData(); - static_cast(_sender)->setCaptionWithReplacing("#{sNone}"); + _sender->castType()->setCaptionWithReplacing("#{sNone}"); MWBase::Environment::get().getWindowManager ()->staticMessageBox ("#{sControlsMenu3}"); MWBase::Environment::get().getWindowManager ()->disallowMouse(); - MWBase::Environment::get().getInputManager ()->enableDetectingBindingMode (actionId); + MWBase::Environment::get().getInputManager ()->enableDetectingBindingMode (actionId, mKeyboardMode); } @@ -508,7 +574,10 @@ namespace MWGui void SettingsWindow::onResetDefaultBindingsAccept() { - MWBase::Environment::get().getInputManager ()->resetToDefaultBindings (); + if(mKeyboardMode) + MWBase::Environment::get().getInputManager ()->resetToDefaultKeyBindings (); + else + MWBase::Environment::get().getInputManager()->resetToDefaultControllerBindings(); updateControlsBox (); } @@ -521,4 +590,9 @@ namespace MWGui { MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Settings); } + + void SettingsWindow::onWindowResize(MyGUI::Window *_sender) + { + updateControlsBox(); + } } diff --git a/apps/openmw/mwgui/settingswindow.hpp b/apps/openmw/mwgui/settingswindow.hpp index cbfb5cfe1..1b970b8de 100644 --- a/apps/openmw/mwgui/settingswindow.hpp +++ b/apps/openmw/mwgui/settingswindow.hpp @@ -28,6 +28,7 @@ namespace MWGui MyGUI::ListBox* mResolutionList; MyGUI::Button* mFullscreenButton; MyGUI::Button* mVSyncButton; + MyGUI::Button* mWindowBorderButton; MyGUI::Button* mFPSButton; MyGUI::ScrollBar* mFOVSlider; MyGUI::ScrollBar* mDifficultySlider; @@ -45,6 +46,9 @@ namespace MWGui // controls MyGUI::ScrollView* mControlsBox; MyGUI::Button* mResetControlsButton; + MyGUI::Button* mKeyboardSwitch; + MyGUI::Button* mControllerSwitch; + bool mKeyboardMode; //if true, setting up the keyboard. Otherwise, it's controller void onOkButtonClicked(MyGUI::Widget* _sender); void onFpsToggled(MyGUI::Widget* _sender); @@ -62,6 +66,10 @@ namespace MWGui void onInputTabMouseWheel(MyGUI::Widget* _sender, int _rel); void onResetDefaultBindings(MyGUI::Widget* _sender); void onResetDefaultBindingsAccept (); + void onKeyboardSwitchClicked(MyGUI::Widget* _sender); + void onControllerSwitchClicked(MyGUI::Widget* _sender); + + void onWindowResize(MyGUI::Window* _sender); void apply(); diff --git a/apps/openmw/mwgui/sortfilteritemmodel.cpp b/apps/openmw/mwgui/sortfilteritemmodel.cpp index 93e5432ca..6c164df88 100644 --- a/apps/openmw/mwgui/sortfilteritemmodel.cpp +++ b/apps/openmw/mwgui/sortfilteritemmodel.cpp @@ -1,5 +1,7 @@ #include "sortfilteritemmodel.hpp" +#include + #include #include #include @@ -41,20 +43,26 @@ namespace return std::find(mapping.begin(), mapping.end(), type1) < std::find(mapping.begin(), mapping.end(), type2); } - bool compare (const MWGui::ItemStack& left, const MWGui::ItemStack& right) + struct Compare { - if (left.mType != right.mType) - return left.mType < right.mType; - - if (left.mBase.getTypeName() == right.mBase.getTypeName()) + bool mSortByType; + Compare() : mSortByType(true) {} + bool operator() (const MWGui::ItemStack& left, const MWGui::ItemStack& right) { - int cmp = left.mBase.getClass().getName(left.mBase).compare( - right.mBase.getClass().getName(right.mBase)); - return cmp < 0; + if (mSortByType && left.mType != right.mType) + return left.mType < right.mType; + + if (left.mBase.getTypeName() == right.mBase.getTypeName()) + { + std::string leftName = Misc::StringUtils::lowerCase(left.mBase.getClass().getName(left.mBase)); + std::string rightName = Misc::StringUtils::lowerCase(right.mBase.getClass().getName(right.mBase)); + + return leftName.compare(rightName) < 0; + } + else + return compareType(left.mBase.getTypeName(), right.mBase.getTypeName()); } - else - return compareType(left.mBase.getTypeName(), right.mBase.getTypeName()); - } + }; } namespace MWGui @@ -63,6 +71,7 @@ namespace MWGui SortFilterItemModel::SortFilterItemModel(ItemModel *sourceModel) : mCategory(Category_All) , mShowEquipped(true) + , mSortByType(true) , mFilter(0) { mSourceModel = sourceModel; @@ -183,7 +192,9 @@ namespace MWGui mItems.push_back(item); } - std::sort(mItems.begin(), mItems.end(), compare); + Compare cmp; + cmp.mSortByType = mSortByType; + std::sort(mItems.begin(), mItems.end(), cmp); } } diff --git a/apps/openmw/mwgui/sortfilteritemmodel.hpp b/apps/openmw/mwgui/sortfilteritemmodel.hpp index 4af35e7a8..1b68bdd4f 100644 --- a/apps/openmw/mwgui/sortfilteritemmodel.hpp +++ b/apps/openmw/mwgui/sortfilteritemmodel.hpp @@ -26,6 +26,9 @@ namespace MWGui void setFilter (int filter); void setShowEquipped (bool show) { mShowEquipped = show; } + /// Use ItemStack::Type for sorting? + void setSortByType(bool sort) { mSortByType = sort; } + static const int Category_Weapon = (1<<1); static const int Category_Apparel = (1<<2); static const int Category_Misc = (1<<3); @@ -47,6 +50,7 @@ namespace MWGui int mCategory; int mFilter; bool mShowEquipped; + bool mSortByType; }; } diff --git a/apps/openmw/mwgui/soulgemdialog.cpp b/apps/openmw/mwgui/soulgemdialog.cpp index 6d70c85d9..0232eb7b3 100644 --- a/apps/openmw/mwgui/soulgemdialog.cpp +++ b/apps/openmw/mwgui/soulgemdialog.cpp @@ -1,6 +1,7 @@ #include "soulgemdialog.hpp" #include "../mwbase/environment.hpp" +#include "../mwbase/windowmanager.hpp" #include "messagebox.hpp" diff --git a/apps/openmw/mwgui/spellbuyingwindow.cpp b/apps/openmw/mwgui/spellbuyingwindow.cpp index 8d9f35daa..54094b606 100644 --- a/apps/openmw/mwgui/spellbuyingwindow.cpp +++ b/apps/openmw/mwgui/spellbuyingwindow.cpp @@ -1,6 +1,8 @@ #include "spellbuyingwindow.hpp" -#include +#include +#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -50,21 +52,22 @@ namespace MWGui MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); int playerGold = player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId); + // TODO: refactor to use MyGUI::ListBox + MyGUI::Button* toAdd = mSpellsView->createWidget( - "SandTextButton", + price <= playerGold ? "SandTextButton" : "SandTextButtonDisabled", // can't use setEnabled since that removes tooltip 0, mCurrentY, 200, sLineHeight, MyGUI::Align::Default ); - toAdd->setEnabled(price<=playerGold); mCurrentY += sLineHeight; toAdd->setUserData(price); - toAdd->setCaptionWithReplacing(spell->mName+" - "+boost::lexical_cast(price)+"#{sgp}"); + toAdd->setCaptionWithReplacing(spell->mName+" - "+MyGUI::utility::toString(price)+"#{sgp}"); toAdd->setSize(toAdd->getTextSize().width,sLineHeight); toAdd->eventMouseWheel += MyGUI::newDelegate(this, &SpellBuyingWindow::onMouseWheel); toAdd->setUserString("ToolTipType", "Spell"); @@ -98,6 +101,15 @@ namespace MWGui if (spell->mData.mType!=ESM::Spell::ST_Spell) continue; // don't try to sell diseases, curses or powers + if (actor.getClass().isNpc()) + { + const ESM::Race* race = + MWBase::Environment::get().getWorld()->getStore().get().find( + actor.get()->mBase->mRace); + if (race->mPowers.exists(spell->mId)) + continue; + } + if (playerHasSpell(iter->first)) continue; @@ -106,7 +118,10 @@ namespace MWGui updateLabels(); + // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden + mSpellsView->setVisibleVScroll(false); mSpellsView->setCanvasSize (MyGUI::IntSize(mSpellsView->getWidth(), std::max(mSpellsView->getHeight(), mCurrentY))); + mSpellsView->setVisibleVScroll(true); } bool SpellBuyingWindow::playerHasSpell(const std::string &id) @@ -126,10 +141,18 @@ namespace MWGui int price = *_sender->getUserData(); MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); + if (price > player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId)) + return; + MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); MWMechanics::Spells& spells = stats.getSpells(); spells.add (mSpellsWidgetMap.find(_sender)->second); player.getClass().getContainerStore(player).remove(MWWorld::ContainerStore::sGoldId, price, player); + + // add gold to NPC trading gold pool + MWMechanics::CreatureStats& npcStats = mPtr.getClass().getCreatureStats(mPtr); + npcStats.setGoldPool(npcStats.getGoldPool() + price); + startSpellBuying(mPtr); MWBase::Environment::get().getSoundManager()->playSound ("Item Gold Up", 1.0, 1.0); @@ -145,7 +168,7 @@ namespace MWGui MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); int playerGold = player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId); - mPlayerGold->setCaptionWithReplacing("#{sGold}: " + boost::lexical_cast(playerGold)); + mPlayerGold->setCaptionWithReplacing("#{sGold}: " + MyGUI::utility::toString(playerGold)); mPlayerGold->setCoord(8, mPlayerGold->getTop(), mPlayerGold->getTextSize().width, diff --git a/apps/openmw/mwgui/spellcreationdialog.cpp b/apps/openmw/mwgui/spellcreationdialog.cpp index 030b8bf37..f4c9d021a 100644 --- a/apps/openmw/mwgui/spellcreationdialog.cpp +++ b/apps/openmw/mwgui/spellcreationdialog.cpp @@ -1,6 +1,11 @@ #include "spellcreationdialog.hpp" -#include +#include +#include + +#include +#include +#include #include "../mwbase/windowmanager.hpp" #include "../mwbase/soundmanager.hpp" @@ -10,6 +15,7 @@ #include "../mwworld/containerstore.hpp" #include "../mwworld/class.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwmechanics/spellcasting.hpp" #include "../mwmechanics/spells.hpp" @@ -17,6 +23,7 @@ #include "tooltips.hpp" #include "class.hpp" +#include "widgets.hpp" namespace { @@ -29,6 +36,18 @@ namespace return gmst.find(ESM::MagicEffect::effectIdToString (id1))->getString() < gmst.find(ESM::MagicEffect::effectIdToString (id2))->getString(); } + + void init(ESM::ENAMstruct& effect) + { + effect.mArea = 0; + effect.mDuration = 0; + effect.mEffectID = -1; + effect.mMagnMax = 0; + effect.mMagnMin = 0; + effect.mRange = 0; + effect.mSkill = -1; + effect.mAttribute = -1; + } } namespace MWGui @@ -37,7 +56,12 @@ namespace MWGui EditEffectDialog::EditEffectDialog() : WindowModal("openmw_edit_effect.layout") , mEditing(false) + , mMagicEffect(NULL) + , mConstantEffect(false) { + init(mEffect); + init(mOldEffect); + getWidget(mCancelButton, "CancelButton"); getWidget(mOkButton, "OkButton"); getWidget(mDeleteButton, "DeleteButton"); @@ -66,7 +90,11 @@ namespace MWGui mMagnitudeMaxSlider->eventScrollChangePosition += MyGUI::newDelegate(this, &EditEffectDialog::onMagnitudeMaxChanged); mDurationSlider->eventScrollChangePosition += MyGUI::newDelegate(this, &EditEffectDialog::onDurationChanged); mAreaSlider->eventScrollChangePosition += MyGUI::newDelegate(this, &EditEffectDialog::onAreaChanged); - constantEffect=false; + } + + void EditEffectDialog::setConstantEffect(bool constant) + { + mConstantEffect = constant; } void EditEffectDialog::open() @@ -86,15 +114,22 @@ namespace MWGui void EditEffectDialog::newEffect (const ESM::MagicEffect *effect) { + bool allowSelf = effect->mData.mFlags & ESM::MagicEffect::CastSelf; + bool allowTouch = (effect->mData.mFlags & ESM::MagicEffect::CastTouch) && !mConstantEffect; + bool allowTarget = (effect->mData.mFlags & ESM::MagicEffect::CastTarget) && !mConstantEffect; + + if (!allowSelf && !allowTouch && !allowTarget) + return; // TODO: Show an error message popup? + setMagicEffect(effect); mEditing = false; mDeleteButton->setVisible (false); mEffect.mRange = ESM::RT_Self; - if (!(mMagicEffect->mData.mFlags & ESM::MagicEffect::CastSelf)) + if (!allowSelf) mEffect.mRange = ESM::RT_Touch; - if (!(mMagicEffect->mData.mFlags & ESM::MagicEffect::CastTouch)) + if (!allowTouch) mEffect.mRange = ESM::RT_Target; mEffect.mMagnMin = 1; mEffect.mMagnMax = 1; @@ -115,6 +150,8 @@ namespace MWGui mMagnitudeMinValue->setCaption("1"); mMagnitudeMaxValue->setCaption("- 1"); mAreaValue->setCaption("0"); + + setVisible(true); } void EditEffectDialog::editEffect (ESM::ENAMstruct effect) @@ -143,13 +180,7 @@ namespace MWGui void EditEffectDialog::setMagicEffect (const ESM::MagicEffect *effect) { - std::string icon = effect->mIcon; - icon[icon.size()-3] = 'd'; - icon[icon.size()-2] = 'd'; - icon[icon.size()-1] = 's'; - icon = "icons\\" + icon; - - mEffectImage->setImageTexture (icon); + mEffectImage->setImageTexture(Misc::ResourceHelpers::correctIconPath(effect->mIcon)); mEffectName->setCaptionWithReplacing("#{"+ESM::MagicEffect::effectIdToString (effect->mIndex)+"}"); @@ -175,7 +206,7 @@ namespace MWGui mMagnitudeBox->setVisible (true); curY += mMagnitudeBox->getSize().height; } - if (!(mMagicEffect->mData.mFlags & ESM::MagicEffect::NoDuration)&&constantEffect==false) + if (!(mMagicEffect->mData.mFlags & ESM::MagicEffect::NoDuration)&&mConstantEffect==false) { mDurationBox->setPosition(mDurationBox->getPosition().left, curY); mDurationBox->setVisible (true); @@ -185,7 +216,7 @@ namespace MWGui { mAreaBox->setPosition(mAreaBox->getPosition().left, curY); mAreaBox->setVisible (true); - curY += mAreaBox->getSize().height; + //curY += mAreaBox->getSize().height; } } @@ -193,26 +224,31 @@ namespace MWGui { mEffect.mRange = (mEffect.mRange+1)%3; - if (mEffect.mRange == ESM::RT_Self) - mRangeButton->setCaptionWithReplacing ("#{sRangeSelf}"); - else if (mEffect.mRange == ESM::RT_Target) - mRangeButton->setCaptionWithReplacing ("#{sRangeTarget}"); - else if (mEffect.mRange == ESM::RT_Touch) - mRangeButton->setCaptionWithReplacing ("#{sRangeTouch}"); - // cycle through range types until we find something that's allowed - if (mEffect.mRange == ESM::RT_Target && !(mMagicEffect->mData.mFlags & ESM::MagicEffect::CastTarget)) - onRangeButtonClicked(sender); - if (mEffect.mRange == ESM::RT_Self && !(mMagicEffect->mData.mFlags & ESM::MagicEffect::CastSelf)) - onRangeButtonClicked(sender); - if (mEffect.mRange == ESM::RT_Touch && !(mMagicEffect->mData.mFlags & ESM::MagicEffect::CastTouch)) - onRangeButtonClicked(sender); + // does not handle the case where nothing is allowed (this should be prevented before opening the Add Effect dialog) + bool allowSelf = mMagicEffect->mData.mFlags & ESM::MagicEffect::CastSelf; + bool allowTouch = (mMagicEffect->mData.mFlags & ESM::MagicEffect::CastTouch) && !mConstantEffect; + bool allowTarget = (mMagicEffect->mData.mFlags & ESM::MagicEffect::CastTarget) && !mConstantEffect; + if (mEffect.mRange == ESM::RT_Self && !allowSelf) + mEffect.mRange = (mEffect.mRange+1)%3; + if (mEffect.mRange == ESM::RT_Touch && !allowTouch) + mEffect.mRange = (mEffect.mRange+1)%3; + if (mEffect.mRange == ESM::RT_Target && !allowTarget) + mEffect.mRange = (mEffect.mRange+1)%3; if(mEffect.mRange == ESM::RT_Self) { mAreaSlider->setScrollPosition(0); onAreaChanged(mAreaSlider,0); } + + if (mEffect.mRange == ESM::RT_Self) + mRangeButton->setCaptionWithReplacing ("#{sRangeSelf}"); + else if (mEffect.mRange == ESM::RT_Target) + mRangeButton->setCaptionWithReplacing ("#{sRangeTarget}"); + else if (mEffect.mRange == ESM::RT_Touch) + mRangeButton->setCaptionWithReplacing ("#{sRangeTouch}"); + updateBoxes(); eventEffectModified(mEffect); } @@ -248,7 +284,7 @@ namespace MWGui void EditEffectDialog::onMagnitudeMinChanged (MyGUI::ScrollBar* sender, size_t pos) { - mMagnitudeMinValue->setCaption(boost::lexical_cast(pos+1)); + mMagnitudeMinValue->setCaption(MyGUI::utility::toString(pos+1)); mEffect.mMagnMin = pos+1; // trigger the check again (see below) @@ -268,21 +304,21 @@ namespace MWGui mEffect.mMagnMax = pos+1; - mMagnitudeMaxValue->setCaption("- " + boost::lexical_cast(pos+1)); + mMagnitudeMaxValue->setCaption("- " + MyGUI::utility::toString(pos+1)); eventEffectModified(mEffect); } void EditEffectDialog::onDurationChanged (MyGUI::ScrollBar* sender, size_t pos) { - mDurationValue->setCaption(boost::lexical_cast(pos+1)); + mDurationValue->setCaption(MyGUI::utility::toString(pos+1)); mEffect.mDuration = pos+1; eventEffectModified(mEffect); } void EditEffectDialog::onAreaChanged (MyGUI::ScrollBar* sender, size_t pos) { - mAreaValue->setCaption(boost::lexical_cast(pos)); + mAreaValue->setCaption(MyGUI::utility::toString(pos)); mEffect.mArea = pos; eventEffectModified(mEffect); } @@ -291,7 +327,7 @@ namespace MWGui SpellCreationDialog::SpellCreationDialog() : WindowBase("openmw_spellcreation_dialog.layout") - , EffectEditorBase() + , EffectEditorBase(EffectEditorBase::Spellmaking) { getWidget(mNameEdit, "NameEdit"); getWidget(mMagickaCost, "MagickaCost"); @@ -344,7 +380,7 @@ namespace MWGui MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); int playerGold = player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId); - if (boost::lexical_cast(mPriceLabel->getCaption()) > playerGold) + if (MyGUI::utility::parseInt(mPriceLabel->getCaption()) > playerGold) { MWBase::Environment::get().getWindowManager()->messageBox ("#{sNotifyMessage18}"); return; @@ -352,9 +388,15 @@ namespace MWGui mSpell.mName = mNameEdit->getCaption(); - player.getClass().getContainerStore(player).remove(MWWorld::ContainerStore::sGoldId, boost::lexical_cast(mPriceLabel->getCaption()), player); + int price = MyGUI::utility::parseInt(mPriceLabel->getCaption()); + + player.getClass().getContainerStore(player).remove(MWWorld::ContainerStore::sGoldId, price, player); - MWBase::Environment::get().getSoundManager()->playSound ("Item Gold Up", 1.0, 1.0); + // add gold to NPC trading gold pool + MWMechanics::CreatureStats& npcStats = mPtr.getClass().getCreatureStats(mPtr); + npcStats.setGoldPool(npcStats.getGoldPool() + price); + + MWBase::Environment::get().getSoundManager()->playSound ("Mysticism Hit", 1.0, 1.0); const ESM::Spell* spell = MWBase::Environment::get().getWorld()->createRecord(mSpell); @@ -362,8 +404,6 @@ namespace MWGui MWMechanics::Spells& spells = stats.getSpells(); spells.add (spell->mId); - MWBase::Environment::get().getSoundManager()->playSound ("Item Gold Up", 1.0, 1.0); - MWBase::Environment::get().getWindowManager()->removeGuiMode (GM_SpellCreation); } @@ -385,6 +425,14 @@ namespace MWGui void SpellCreationDialog::notifyEffectsChanged () { + if (mEffects.empty()) + { + mMagickaCost->setCaption("0"); + mPriceLabel->setCaption("0"); + mSuccessChance->setCaption("0"); + return; + } + float y = 0; const MWWorld::ESMStore &store = @@ -392,7 +440,7 @@ namespace MWGui for (std::vector::const_iterator it = mEffects.begin(); it != mEffects.end(); ++it) { - float x = 0.5 * it->mMagnMin + it->mMagnMax; + float x = 0.5 * (it->mMagnMin + it->mMagnMax); const ESM::MagicEffect* effect = store.get().find(it->mEffectID); @@ -407,37 +455,43 @@ namespace MWGui y += x * fEffectCostMult; y = std::max(1.f,y); - if (effect->mData.mFlags & ESM::MagicEffect::CastTarget) + if (it->mRange == ESM::RT_Target) y *= 1.5; } - mSpell.mData.mCost = int(y); - ESM::EffectList effectList; effectList.mList = mEffects; mSpell.mEffects = effectList; + mSpell.mData.mCost = int(y); mSpell.mData.mType = ESM::Spell::ST_Spell; + mSpell.mData.mFlags = 0; - mMagickaCost->setCaption(boost::lexical_cast(int(y))); + mMagickaCost->setCaption(MyGUI::utility::toString(int(y))); float fSpellMakingValueMult = store.get().find("fSpellMakingValueMult")->getFloat(); int price = MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mPtr,int(y) * fSpellMakingValueMult,true); - mPriceLabel->setCaption(boost::lexical_cast(int(price))); + mPriceLabel->setCaption(MyGUI::utility::toString(int(price))); float chance = MWMechanics::getSpellSuccessChance(&mSpell, MWBase::Environment::get().getWorld()->getPlayerPtr()); - mSuccessChance->setCaption(boost::lexical_cast(int(chance))); + mSuccessChance->setCaption(MyGUI::utility::toString(int(chance))); } // ------------------------------------------------------------------------------------------------ - EffectEditorBase::EffectEditorBase() + EffectEditorBase::EffectEditorBase(Type type) : mAddEffectDialog() + , mAvailableEffectsList(NULL) + , mUsedEffectsView(NULL) , mSelectAttributeDialog(NULL) , mSelectSkillDialog(NULL) + , mSelectedEffect(0) + , mSelectedKnownEffectId(0) + , mType(type) + , mConstantEffect(false) { mAddEffectDialog.eventEffectAdded += MyGUI::newDelegate(this, &EffectEditorBase::onEffectAdded); mAddEffectDialog.eventEffectModified += MyGUI::newDelegate(this, &EffectEditorBase::onEffectModified); @@ -472,6 +526,13 @@ namespace MWGui const std::vector& list = spell->mEffects.mList; for (std::vector::const_iterator it2 = list.begin(); it2 != list.end(); ++it2) { + const ESM::MagicEffect * effect = MWBase::Environment::get().getWorld()->getStore().get().find(it2->mEffectID); + + // skip effects that do not allow spellmaking/enchanting + int requiredFlags = (mType == Spellmaking) ? ESM::MagicEffect::AllowSpellmaking : ESM::MagicEffect::AllowEnchanting; + if (!(effect->mData.mFlags & requiredFlags)) + continue; + if (std::find(knownEffects.begin(), knownEffects.end(), it2->mEffectID) == knownEffects.end()) knownEffects.push_back(it2->mEffectID); } @@ -504,7 +565,7 @@ namespace MWGui updateEffectsView (); } - void EffectEditorBase::setWidgets (Widgets::MWList *availableEffectsList, MyGUI::ScrollView *usedEffectsView) + void EffectEditorBase::setWidgets (Gui::MWList *availableEffectsList, MyGUI::ScrollView *usedEffectsView) { mAvailableEffectsList = availableEffectsList; mUsedEffectsView = usedEffectsView; @@ -514,7 +575,10 @@ namespace MWGui void EffectEditorBase::onSelectAttribute () { - mAddEffectDialog.setVisible(true); + const ESM::MagicEffect* effect = + MWBase::Environment::get().getWorld()->getStore().get().find(mSelectedKnownEffectId); + + mAddEffectDialog.newEffect(effect); mAddEffectDialog.setAttribute (mSelectAttributeDialog->getAttributeId()); MWBase::Environment::get().getWindowManager ()->removeDialog (mSelectAttributeDialog); mSelectAttributeDialog = 0; @@ -522,8 +586,11 @@ namespace MWGui void EffectEditorBase::onSelectSkill () { - mAddEffectDialog.setVisible(true); - mAddEffectDialog.setSkill (mSelectSkillDialog->getSkillId ()); + const ESM::MagicEffect* effect = + MWBase::Environment::get().getWorld()->getStore().get().find(mSelectedKnownEffectId); + + mAddEffectDialog.newEffect(effect); + mAddEffectDialog.setSkill (mSelectSkillDialog->getSkillId()); MWBase::Environment::get().getWindowManager ()->removeDialog (mSelectSkillDialog); mSelectSkillDialog = 0; } @@ -548,21 +615,10 @@ namespace MWGui } int buttonId = *sender->getUserData(); - short effectId = mButtonMapping[buttonId]; - - for (std::vector::const_iterator it = mEffects.begin(); it != mEffects.end(); ++it) - { - if (it->mEffectID == effectId) - { - MWBase::Environment::get().getWindowManager()->messageBox ("#{sOnetypeEffectMessage}"); - return; - } - } + mSelectedKnownEffectId = mButtonMapping[buttonId]; const ESM::MagicEffect* effect = - MWBase::Environment::get().getWorld()->getStore().get().find(effectId); - - mAddEffectDialog.newEffect (effect); + MWBase::Environment::get().getWorld()->getStore().get().find(mSelectedKnownEffectId); if (effect->mData.mFlags & ESM::MagicEffect::TargetSkill) { @@ -582,7 +638,16 @@ namespace MWGui } else { - mAddEffectDialog.setVisible(true); + for (std::vector::const_iterator it = mEffects.begin(); it != mEffects.end(); ++it) + { + if (it->mEffectID == mSelectedKnownEffectId) + { + MWBase::Environment::get().getWindowManager()->messageBox ("#{sOnetypeEffectMessage}"); + return; + } + } + + mAddEffectDialog.newEffect(effect); } } @@ -618,6 +683,7 @@ namespace MWGui params.mMagnMax = it->mMagnMax; params.mRange = it->mRange; params.mArea = it->mArea; + params.mIsConstant = mConstantEffect; MyGUI::Button* button = mUsedEffectsView->createWidget("", MyGUI::IntCoord(0, size.height, 0, 24), MyGUI::Align::Default); button->setUserData(i); @@ -637,7 +703,10 @@ namespace MWGui ++i; } + // Canvas size must be expressed with HScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden + mUsedEffectsView->setVisibleHScroll(false); mUsedEffectsView->setCanvasSize(size); + mUsedEffectsView->setVisibleHScroll(true); notifyEffectsChanged(); } @@ -659,4 +728,10 @@ namespace MWGui mAddEffectDialog.editEffect (mEffects[id]); mAddEffectDialog.setVisible (true); } + + void EffectEditorBase::setConstantEffect(bool constant) + { + mAddEffectDialog.setConstantEffect(constant); + mConstantEffect = constant; + } } diff --git a/apps/openmw/mwgui/spellcreationdialog.hpp b/apps/openmw/mwgui/spellcreationdialog.hpp index 25c615678..6cdf74d10 100644 --- a/apps/openmw/mwgui/spellcreationdialog.hpp +++ b/apps/openmw/mwgui/spellcreationdialog.hpp @@ -1,10 +1,16 @@ #ifndef MWGUI_SPELLCREATION_H #define MWGUI_SPELLCREATION_H +#include +#include + #include "windowbase.hpp" #include "referenceinterface.hpp" -#include "list.hpp" -#include "widgets.hpp" + +namespace Gui +{ + class MWList; +} namespace MWGui { @@ -20,12 +26,13 @@ namespace MWGui virtual void open(); virtual void exit(); + void setConstantEffect(bool constant); + void setSkill(int skill); void setAttribute(int attribute); void newEffect (const ESM::MagicEffect* effect); void editEffect (ESM::ENAMstruct effect); - bool constantEffect; typedef MyGUI::delegates::CMultiDelegate1 EventHandle_Effect; EventHandle_Effect eventEffectAdded; @@ -79,19 +86,29 @@ namespace MWGui ESM::ENAMstruct mOldEffect; const ESM::MagicEffect* mMagicEffect; + + bool mConstantEffect; }; class EffectEditorBase { public: - EffectEditorBase(); + enum Type + { + Spellmaking, + Enchanting + }; + + EffectEditorBase(Type type); virtual ~EffectEditorBase(); + void setConstantEffect(bool constant); + protected: std::map mButtonMapping; // maps button ID to effect ID - Widgets::MWList* mAvailableEffectsList; + Gui::MWList* mAvailableEffectsList; MyGUI::ScrollView* mUsedEffectsView; EditEffectDialog mAddEffectDialog; @@ -99,6 +116,9 @@ namespace MWGui SelectSkillDialog* mSelectSkillDialog; int mSelectedEffect; + short mSelectedKnownEffectId; + + bool mConstantEffect; std::vector mEffects; @@ -117,9 +137,12 @@ namespace MWGui void updateEffectsView(); void startEditing(); - void setWidgets (Widgets::MWList* availableEffectsList, MyGUI::ScrollView* usedEffectsView); + void setWidgets (Gui::MWList* availableEffectsList, MyGUI::ScrollView* usedEffectsView); virtual void notifyEffectsChanged () {} + + private: + Type mType; }; class SpellCreationDialog : public WindowBase, public ReferenceInterface, public EffectEditorBase @@ -147,8 +170,6 @@ namespace MWGui MyGUI::Button* mCancelButton; MyGUI::TextBox* mPriceLabel; - Widgets::MWEffectList* mUsedEffectsList; - ESM::Spell mSpell; }; diff --git a/apps/openmw/mwgui/spellicons.cpp b/apps/openmw/mwgui/spellicons.cpp index 1a9e418de..20c6f3ba8 100644 --- a/apps/openmw/mwgui/spellicons.cpp +++ b/apps/openmw/mwgui/spellicons.cpp @@ -1,15 +1,19 @@ #include "spellicons.hpp" -#include - #include #include +#include + +#include +#include + #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/class.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwmechanics/creaturestats.hpp" @@ -21,8 +25,8 @@ namespace MWGui { void EffectSourceVisitor::visit (MWMechanics::EffectKey key, - const std::string& sourceName, int casterActorId, - float magnitude, float remainingTime) + const std::string& sourceName, const std::string& sourceId, int casterActorId, + float magnitude, float remainingTime, float totalTime) { MagicEffectInfo newEffectSource; newEffectSource.mKey = key; @@ -30,6 +34,7 @@ namespace MWGui newEffectSource.mPermanent = mIsPermanent; newEffectSource.mRemainingTime = remainingTime; newEffectSource.mSource = sourceName; + newEffectSource.mTotalTime = totalTime; mEffectSources[key.mId].push_back(newEffectSource); } @@ -65,10 +70,11 @@ namespace MWGui MWBase::Environment::get().getWorld ()->getStore ().get().find(it->first); float remainingDuration = 0; + float totalDuration = 0; std::string sourcesDescription; - const float fadeTime = 5.f; + static const float fadeTime = MWBase::Environment::get().getWorld()->getStore().get().find("fMagicStartIconBlink")->getFloat(); for (std::vector::const_iterator effectIt = it->second.begin(); effectIt != it->second.end(); ++effectIt) @@ -78,9 +84,15 @@ namespace MWGui // if at least one of the effect sources is permanent, the effect will never wear off if (effectIt->mPermanent) + { remainingDuration = fadeTime; + totalDuration = fadeTime; + } else + { remainingDuration = std::max(remainingDuration, effectIt->mRemainingTime); + totalDuration = std::max(totalDuration, effectIt->mTotalTime); + } sourcesDescription += effectIt->mSource; @@ -103,7 +115,7 @@ namespace MWGui } else if ( displayType != ESM::MagicEffect::MDT_None ) { - sourcesDescription += ": " + boost::lexical_cast(effectIt->mMagnitude); + sourcesDescription += ": " + MyGUI::utility::toString(effectIt->mMagnitude); if ( displayType == ESM::MagicEffect::MDT_Percentage ) sourcesDescription += MWBase::Environment::get().getWindowManager()->getGameSettingString("spercent", ""); @@ -133,13 +145,7 @@ namespace MWGui ("ImageBox", MyGUI::IntCoord(w,2,16,16), MyGUI::Align::Default); mWidgetMap[it->first] = image; - std::string icon = effect->mIcon; - icon[icon.size()-3] = 'd'; - icon[icon.size()-2] = 'd'; - icon[icon.size()-1] = 's'; - icon = "icons\\" + icon; - - image->setImageTexture(icon); + image->setImageTexture(Misc::ResourceHelpers::correctIconPath(effect->mIcon)); std::string name = ESM::MagicEffect::effectIdToString (it->first); @@ -162,8 +168,9 @@ namespace MWGui ToolTipInfo* tooltipInfo = image->getUserData(); tooltipInfo->text = sourcesDescription; - // Fade out during the last 5 seconds - image->setAlpha(std::min(remainingDuration/fadeTime, 1.f)); + // Fade out + if (totalDuration >= fadeTime && fadeTime > 0.f) + image->setAlpha(std::min(remainingDuration/fadeTime, 1.f)); } else if (mWidgetMap.find(it->first) != mWidgetMap.end()) { diff --git a/apps/openmw/mwgui/spellicons.hpp b/apps/openmw/mwgui/spellicons.hpp index 7df9ad8b9..5099fc4d6 100644 --- a/apps/openmw/mwgui/spellicons.hpp +++ b/apps/openmw/mwgui/spellicons.hpp @@ -26,12 +26,14 @@ namespace MWGui MagicEffectInfo() : mPermanent(false) , mMagnitude(0) - , mRemainingTime(0) + , mRemainingTime(0.f) + , mTotalTime(0.f) {} std::string mSource; // display name for effect source (e.g. potion name) MWMechanics::EffectKey mKey; int mMagnitude; float mRemainingTime; + float mTotalTime; bool mPermanent; // the effect is permanent }; @@ -45,8 +47,8 @@ namespace MWGui virtual ~EffectSourceVisitor() {} virtual void visit (MWMechanics::EffectKey key, - const std::string& sourceName, int casterActorId, - float magnitude, float remainingTime = -1); + const std::string& sourceName, const std::string& sourceId, int casterActorId, + float magnitude, float remainingTime = -1, float totalTime = -1); }; class SpellIcons diff --git a/apps/openmw/mwgui/spellmodel.cpp b/apps/openmw/mwgui/spellmodel.cpp new file mode 100644 index 000000000..4713720cd --- /dev/null +++ b/apps/openmw/mwgui/spellmodel.cpp @@ -0,0 +1,135 @@ +#include "spellmodel.hpp" + +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" +#include "../mwbase/windowmanager.hpp" + +#include "../mwmechanics/creaturestats.hpp" +#include "../mwmechanics/spellcasting.hpp" + +#include "../mwworld/esmstore.hpp" +#include "../mwworld/inventorystore.hpp" +#include "../mwworld/class.hpp" + +namespace +{ + + bool sortSpells(const MWGui::Spell& left, const MWGui::Spell& right) + { + if (left.mType != right.mType) + return left.mType < right.mType; + + std::string leftName = Misc::StringUtils::lowerCase(left.mName); + std::string rightName = Misc::StringUtils::lowerCase(right.mName); + + return leftName.compare(rightName) < 0; + } + +} + +namespace MWGui +{ + + SpellModel::SpellModel(const MWWorld::Ptr &actor) + : mActor(actor) + { + + } + + void SpellModel::update() + { + mSpells.clear(); + + MWMechanics::CreatureStats& stats = mActor.getClass().getCreatureStats(mActor); + const MWMechanics::Spells& spells = stats.getSpells(); + + const MWWorld::ESMStore &esmStore = + MWBase::Environment::get().getWorld()->getStore(); + + for (MWMechanics::Spells::TIterator it = spells.begin(); it != spells.end(); ++it) + { + const ESM::Spell* spell = esmStore.get().find(it->first); + if (spell->mData.mType != ESM::Spell::ST_Power && spell->mData.mType != ESM::Spell::ST_Spell) + continue; + + Spell newSpell; + newSpell.mName = spell->mName; + if (spell->mData.mType == ESM::Spell::ST_Spell) + { + newSpell.mType = Spell::Type_Spell; + std::string cost = boost::lexical_cast(spell->mData.mCost); + std::string chance = boost::lexical_cast(int(MWMechanics::getSpellSuccessChance(spell, mActor))); + newSpell.mCostColumn = cost + "/" + chance; + } + else + newSpell.mType = Spell::Type_Power; + newSpell.mId = it->first; + + newSpell.mSelected = (MWBase::Environment::get().getWindowManager()->getSelectedSpell() == it->first); + newSpell.mActive = true; + mSpells.push_back(newSpell); + } + + MWWorld::InventoryStore& invStore = mActor.getClass().getInventoryStore(mActor); + for (MWWorld::ContainerStoreIterator it = invStore.begin(); it != invStore.end(); ++it) + { + MWWorld::Ptr item = *it; + const std::string enchantId = item.getClass().getEnchantment(item); + if (enchantId.empty()) + continue; + const ESM::Enchantment* enchant = + esmStore.get().find(item.getClass().getEnchantment(item)); + if (enchant->mData.mType != ESM::Enchantment::WhenUsed && enchant->mData.mType != ESM::Enchantment::CastOnce) + continue; + + Spell newSpell; + newSpell.mItem = item; + newSpell.mId = item.getClass().getId(item); + newSpell.mName = item.getClass().getName(item); + newSpell.mType = Spell::Type_EnchantedItem; + newSpell.mSelected = invStore.getSelectedEnchantItem() == it; + + // FIXME: move to mwmechanics + if (enchant->mData.mType == ESM::Enchantment::CastOnce) + { + newSpell.mCostColumn = "100/100"; + newSpell.mActive = false; + } + else + { + if (!item.getClass().getEquipmentSlots(item).first.empty() + && item.getClass().canBeEquipped(item, mActor).first == 0) + continue; + + int castCost = MWMechanics::getEffectiveEnchantmentCastCost(enchant->mData.mCost, mActor); + + std::string cost = boost::lexical_cast(castCost); + int currentCharge = int(item.getCellRef().getEnchantmentCharge()); + if (currentCharge == -1) + currentCharge = enchant->mData.mCharge; + std::string charge = boost::lexical_cast(currentCharge); + newSpell.mCostColumn = cost + "/" + charge; + + newSpell.mActive = invStore.isEquipped(item); + } + mSpells.push_back(newSpell); + } + + std::stable_sort(mSpells.begin(), mSpells.end(), sortSpells); + } + + size_t SpellModel::getItemCount() const + { + return mSpells.size(); + } + + Spell SpellModel::getItem(ModelIndex index) const + { + if (index < 0 || index >= int(mSpells.size())) + throw std::runtime_error("invalid spell index supplied"); + return mSpells[index]; + } + +} diff --git a/apps/openmw/mwgui/spellmodel.hpp b/apps/openmw/mwgui/spellmodel.hpp new file mode 100644 index 000000000..7859c8a7b --- /dev/null +++ b/apps/openmw/mwgui/spellmodel.hpp @@ -0,0 +1,57 @@ +#ifndef OPENMW_GUI_SPELLMODEL_H +#define OPENMW_GUI_SPELLMODEL_H + +#include "../mwworld/ptr.hpp" + +namespace MWGui +{ + + struct Spell + { + enum Type + { + Type_Power, + Type_Spell, + Type_EnchantedItem + }; + + Type mType; + std::string mName; + std::string mCostColumn; // Cost/chance or Cost/charge + std::string mId; // Item ID or spell ID + MWWorld::Ptr mItem; // Only for Type_EnchantedItem + bool mSelected; // Is this the currently selected spell/item (only one can be selected at a time) + bool mActive; // (Items only) is the item equipped? + + Spell() + : mSelected(false) + , mActive(false) + , mType(Type_Spell) + { + } + }; + + ///@brief Model that lists all usable powers, spells and enchanted items for an actor. + class SpellModel + { + public: + SpellModel(const MWWorld::Ptr& actor); + + typedef int ModelIndex; + + void update(); + + Spell getItem (ModelIndex index) const; + ///< throws for invalid index + + size_t getItemCount() const; + + private: + MWWorld::Ptr mActor; + + std::vector mSpells; + }; + +} + +#endif diff --git a/apps/openmw/mwgui/spellview.cpp b/apps/openmw/mwgui/spellview.cpp new file mode 100644 index 000000000..1c17a11f6 --- /dev/null +++ b/apps/openmw/mwgui/spellview.cpp @@ -0,0 +1,245 @@ +#include "spellview.hpp" + +#include +#include +#include +#include + +#include + +namespace MWGui +{ + + SpellView::SpellView() + : mShowCostColumn(true) + , mHighlightSelected(true) + , mScrollView(NULL) + { + } + + void SpellView::initialiseOverride() + { + Base::initialiseOverride(); + + assignWidget(mScrollView, "ScrollView"); + if (mScrollView == NULL) + throw std::runtime_error("Item view needs a scroll view"); + + mScrollView->setCanvasAlign(MyGUI::Align::Left | MyGUI::Align::Top); + } + + void SpellView::registerComponents() + { + MyGUI::FactoryManager::getInstance().registerFactory("Widget"); + } + + void SpellView::setModel(SpellModel *model) + { + mModel.reset(model); + update(); + } + + SpellModel* SpellView::getModel() + { + return mModel.get(); + } + + void SpellView::setShowCostColumn(bool show) + { + if (show != mShowCostColumn) + { + mShowCostColumn = show; + update(); + } + } + + void SpellView::setHighlightSelected(bool highlight) + { + if (highlight != mHighlightSelected) + { + mHighlightSelected = highlight; + update(); + } + } + + void SpellView::update() + { + if (!mModel.get()) + return; + + mModel->update(); + + int curType = -1; + + const int spellHeight = 18; + + mLines.clear(); + + while (mScrollView->getChildCount()) + MyGUI::Gui::getInstance().destroyWidget(mScrollView->getChildAt(0)); + + for (SpellModel::ModelIndex i = 0; igetItemCount()); ++i) + { + const Spell& spell = mModel->getItem(i); + if (curType != spell.mType) + { + if (spell.mType == Spell::Type_Power) + addGroup("#{sPowers}", ""); + else if (spell.mType == Spell::Type_Spell) + addGroup("#{sSpells}", "#{sCostChance}"); + else + addGroup("#{sMagicItem}", "#{sCostCharge}"); + curType = spell.mType; + } + + const std::string skin = spell.mActive ? "SandTextButton" : "SpellTextUnequipped"; + + Gui::SharedStateButton* t = mScrollView->createWidget(skin, + MyGUI::IntCoord(0, 0, 0, spellHeight), MyGUI::Align::Left | MyGUI::Align::Top); + t->setCaption(spell.mName); + t->setTextAlign(MyGUI::Align::Left); + adjustSpellWidget(spell, i, t); + + if (!spell.mCostColumn.empty() && mShowCostColumn) + { + Gui::SharedStateButton* costChance = mScrollView->createWidget(skin, + MyGUI::IntCoord(0, 0, 0, spellHeight), MyGUI::Align::Left | MyGUI::Align::Top); + costChance->setCaption(spell.mCostColumn); + costChance->setTextAlign(MyGUI::Align::Right); + adjustSpellWidget(spell, i, costChance); + + Gui::ButtonGroup group; + group.push_back(t); + group.push_back(costChance); + Gui::SharedStateButton::createButtonGroup(group); + + mLines.push_back(std::make_pair(t, costChance)); + } + else + mLines.push_back(std::make_pair(t, (MyGUI::Widget*)NULL)); + + t->setStateSelected(spell.mSelected); + } + + layoutWidgets(); + } + + void SpellView::layoutWidgets() + { + int height = 0; + for (std::vector< std::pair >::iterator it = mLines.begin(); + it != mLines.end(); ++it) + { + height += (it->first)->getHeight(); + } + + bool scrollVisible = height > mScrollView->getHeight(); + int width = mScrollView->getWidth() - (scrollVisible ? 18 : 0); + + height = 0; + for (std::vector< std::pair >::iterator it = mLines.begin(); + it != mLines.end(); ++it) + { + int lineHeight = (it->first)->getHeight(); + (it->first)->setCoord(4, height, width-8, lineHeight); + if (it->second) + { + (it->second)->setCoord(4, height, width-8, lineHeight); + MyGUI::TextBox* second = (it->second)->castType(false); + if (second) + (it->first)->setSize(width-8-second->getTextSize().width, lineHeight); + } + + height += lineHeight; + } + + // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden + mScrollView->setVisibleVScroll(false); + mScrollView->setCanvasSize(mScrollView->getWidth(), std::max(mScrollView->getHeight(), height)); + mScrollView->setVisibleVScroll(true); + } + + void SpellView::addGroup(const std::string &label, const std::string& label2) + { + if (mScrollView->getChildCount() > 0) + { + MyGUI::ImageBox* separator = mScrollView->createWidget("MW_HLine", + MyGUI::IntCoord(0, 0, mScrollView->getWidth(), 18), + MyGUI::Align::Left | MyGUI::Align::Top); + separator->setNeedMouseFocus(false); + mLines.push_back(std::make_pair(separator, (MyGUI::Widget*)NULL)); + } + + MyGUI::TextBox* groupWidget = mScrollView->createWidget("SandBrightText", + MyGUI::IntCoord(0, 0, mScrollView->getWidth(), 24), + MyGUI::Align::Left | MyGUI::Align::Top); + groupWidget->setCaptionWithReplacing(label); + groupWidget->setTextAlign(MyGUI::Align::Left); + groupWidget->setNeedMouseFocus(false); + + if (label2 != "") + { + MyGUI::TextBox* groupWidget2 = mScrollView->createWidget("SandBrightText", + MyGUI::IntCoord(0, 0, mScrollView->getWidth(), 24), + MyGUI::Align::Left | MyGUI::Align::Top); + groupWidget2->setCaptionWithReplacing(label2); + groupWidget2->setTextAlign(MyGUI::Align::Right); + groupWidget2->setNeedMouseFocus(false); + + mLines.push_back(std::make_pair(groupWidget, groupWidget2)); + } + else + mLines.push_back(std::make_pair(groupWidget, (MyGUI::Widget*)NULL)); + } + + + void SpellView::setSize(const MyGUI::IntSize &_value) + { + bool changed = (_value.width != getWidth() || _value.height != getHeight()); + Base::setSize(_value); + if (changed) + layoutWidgets(); + } + + void SpellView::setCoord(const MyGUI::IntCoord &_value) + { + bool changed = (_value.width != getWidth() || _value.height != getHeight()); + Base::setCoord(_value); + if (changed) + layoutWidgets(); + } + + void SpellView::adjustSpellWidget(const Spell &spell, SpellModel::ModelIndex index, MyGUI::Widget *widget) + { + if (spell.mType == Spell::Type_EnchantedItem) + { + widget->setUserData(spell.mItem); + widget->setUserString("ToolTipType", "ItemPtr"); + } + else + { + widget->setUserString("ToolTipType", "Spell"); + widget->setUserString("Spell", spell.mId); + } + + widget->setUserString("SpellModelIndex", MyGUI::utility::toString(index)); + + widget->eventMouseWheel += MyGUI::newDelegate(this, &SpellView::onMouseWheel); + widget->eventMouseButtonClick += MyGUI::newDelegate(this, &SpellView::onSpellSelected); + } + + void SpellView::onSpellSelected(MyGUI::Widget* _sender) + { + SpellModel::ModelIndex i = MyGUI::utility::parseInt(_sender->getUserString("SpellModelIndex")); + eventSpellClicked(i); + } + + void SpellView::onMouseWheel(MyGUI::Widget* _sender, int _rel) + { + if (mScrollView->getViewOffset().top + _rel*0.3 > 0) + mScrollView->setViewOffset(MyGUI::IntPoint(0, 0)); + else + mScrollView->setViewOffset(MyGUI::IntPoint(0, mScrollView->getViewOffset().top + _rel*0.3)); + } + +} diff --git a/apps/openmw/mwgui/spellview.hpp b/apps/openmw/mwgui/spellview.hpp new file mode 100644 index 000000000..005d206f4 --- /dev/null +++ b/apps/openmw/mwgui/spellview.hpp @@ -0,0 +1,69 @@ +#ifndef OPENMW_GUI_SPELLVIEW_H +#define OPENMW_GUI_SPELLVIEW_H + +#include + +#include "spellmodel.hpp" + +namespace MyGUI +{ + class ScrollView; +} + +namespace MWGui +{ + + class SpellModel; + + ///@brief Displays a SpellModel in a list widget + class SpellView : public MyGUI::Widget + { + MYGUI_RTTI_DERIVED(SpellView) + public: + SpellView(); + + /// Register needed components with MyGUI's factory manager + static void registerComponents (); + + /// Should the cost/chance column be shown? + void setShowCostColumn(bool show); + + void setHighlightSelected(bool highlight); + + /// Takes ownership of \a model + void setModel (SpellModel* model); + + SpellModel* getModel(); + + void update(); + + typedef MyGUI::delegates::CMultiDelegate1 EventHandle_ModelIndex; + /// Fired when a spell was clicked + EventHandle_ModelIndex eventSpellClicked; + + virtual void initialiseOverride(); + + virtual void setSize(const MyGUI::IntSize& _value); + virtual void setCoord(const MyGUI::IntCoord& _value); + + private: + MyGUI::ScrollView* mScrollView; + + std::auto_ptr mModel; + + std::vector< std::pair > mLines; + + bool mShowCostColumn; + bool mHighlightSelected; + + void layoutWidgets(); + void addGroup(const std::string& label1, const std::string& label2); + void adjustSpellWidget(const Spell& spell, SpellModel::ModelIndex index, MyGUI::Widget* widget); + + void onSpellSelected(MyGUI::Widget* _sender); + void onMouseWheel(MyGUI::Widget* _sender, int _rel); + }; + +} + +#endif diff --git a/apps/openmw/mwgui/spellwindow.cpp b/apps/openmw/mwgui/spellwindow.cpp index 77da56fa4..cc032691e 100644 --- a/apps/openmw/mwgui/spellwindow.cpp +++ b/apps/openmw/mwgui/spellwindow.cpp @@ -1,14 +1,16 @@ #include "spellwindow.hpp" -#include #include +#include + #include "../mwbase/windowmanager.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/class.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwmechanics/spellcasting.hpp" #include "../mwmechanics/spells.hpp" @@ -17,43 +19,24 @@ #include "spellicons.hpp" #include "inventorywindow.hpp" #include "confirmationdialog.hpp" +#include "spellview.hpp" namespace MWGui { - bool sortItems(const MWWorld::Ptr& left, const MWWorld::Ptr& right) - { - int cmp = left.getClass().getName(left).compare( - right.getClass().getName(right)); - return cmp < 0; - } - - bool sortSpells(const std::string& left, const std::string& right) - { - const MWWorld::Store &spells = - MWBase::Environment::get().getWorld()->getStore().get(); - - const ESM::Spell* a = spells.find(left); - const ESM::Spell* b = spells.find(right); - - int cmp = a->mName.compare(b->mName); - return cmp < 0; - } - SpellWindow::SpellWindow(DragAndDrop* drag) : WindowPinnableBase("openmw_spell_window.layout") , NoDrop(drag, mMainWidget) - , mHeight(0) - , mWidth(0) + , mSpellView(NULL) { mSpellIcons = new SpellIcons(); getWidget(mSpellView, "SpellView"); getWidget(mEffectBox, "EffectsBox"); - setCoord(498, 300, 302, 300); + mSpellView->eventSpellClicked += MyGUI::newDelegate(this, &SpellWindow::onModelIndexSelected); - mMainWidget->castType()->eventWindowChangeCoord += MyGUI::newDelegate(this, &SpellWindow::onWindowResize); + setCoord(498, 300, 302, 300); } SpellWindow::~SpellWindow() @@ -66,6 +49,12 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->setSpellVisibility(!mPinned); } + void SpellWindow::onTitleDoubleClicked() + { + if (!mPinned) + MWBase::Environment::get().getWindowManager()->toggleVisible(GW_Magic); + } + void SpellWindow::open() { updateSpells(); @@ -75,238 +64,14 @@ namespace MWGui { mSpellIcons->updateWidgets(mEffectBox, false); - const int spellHeight = 18; - - mHeight = 0; - while (mSpellView->getChildCount()) - MyGUI::Gui::getInstance().destroyWidget(mSpellView->getChildAt(0)); - - // retrieve all player spells, divide them into Powers and Spells and sort them - std::vector spellList; - MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); - MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); - MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); - MWMechanics::Spells& spells = stats.getSpells(); - - for (MWMechanics::Spells::TIterator it = spells.begin(); it != spells.end(); ++it) - spellList.push_back (it->first); - - const MWWorld::ESMStore &esmStore = - MWBase::Environment::get().getWorld()->getStore(); - - std::vector powers; - std::vector::iterator it = spellList.begin(); - while (it != spellList.end()) - { - const ESM::Spell* spell = esmStore.get().find(*it); - - if (spell->mData.mType == ESM::Spell::ST_Power) - { - powers.push_back(*it); - it = spellList.erase(it); - } - else if (spell->mData.mType == ESM::Spell::ST_Ability - || spell->mData.mType == ESM::Spell::ST_Blight - || spell->mData.mType == ESM::Spell::ST_Curse - || spell->mData.mType == ESM::Spell::ST_Disease) - { - it = spellList.erase(it); - } - else - ++it; - } - std::sort(powers.begin(), powers.end(), sortSpells); - std::sort(spellList.begin(), spellList.end(), sortSpells); - - // retrieve player's enchanted items - std::vector items; - for (MWWorld::ContainerStoreIterator it(store.begin()); it != store.end(); ++it) - { - std::string enchantId = it->getClass().getEnchantment(*it); - if (enchantId != "") - { - // only add items with "Cast once" or "Cast on use" - const ESM::Enchantment* enchant = - esmStore.get().find(enchantId); - - int type = enchant->mData.mType; - if (type != ESM::Enchantment::CastOnce - && type != ESM::Enchantment::WhenUsed) - continue; - - items.push_back(*it); - } - } - std::sort(items.begin(), items.end(), sortItems); - - - int height = estimateHeight(items.size() + powers.size() + spellList.size()); - bool scrollVisible = height > mSpellView->getHeight(); - mWidth = mSpellView->getWidth() - (scrollVisible ? 18 : 0); - - // powers - addGroup("#{sPowers}", ""); - - for (std::vector::const_iterator it = powers.begin(); it != powers.end(); ++it) - { - const ESM::Spell* spell = esmStore.get().find(*it); - MyGUI::Button* t = mSpellView->createWidget("SpellText", - MyGUI::IntCoord(4, mHeight, mWidth-8, spellHeight), MyGUI::Align::Left | MyGUI::Align::Top); - t->setCaption(spell->mName); - t->setTextAlign(MyGUI::Align::Left); - t->setUserString("ToolTipType", "Spell"); - t->setUserString("Spell", *it); - t->eventMouseWheel += MyGUI::newDelegate(this, &SpellWindow::onMouseWheel); - t->eventMouseButtonClick += MyGUI::newDelegate(this, &SpellWindow::onSpellSelected); - - if (*it == MWBase::Environment::get().getWindowManager()->getSelectedSpell()) - t->setStateSelected(true); - - mHeight += spellHeight; - } - - // other spells - addGroup("#{sSpells}", "#{sCostChance}"); - for (std::vector::const_iterator it = spellList.begin(); it != spellList.end(); ++it) - { - const ESM::Spell* spell = esmStore.get().find(*it); - MyGUI::Button* t = mSpellView->createWidget("SpellText", - MyGUI::IntCoord(4, mHeight, mWidth-8, spellHeight), MyGUI::Align::Left | MyGUI::Align::Top); - t->setCaption(spell->mName); - t->setTextAlign(MyGUI::Align::Left); - t->setUserString("ToolTipType", "Spell"); - t->setUserString("Spell", *it); - t->eventMouseWheel += MyGUI::newDelegate(this, &SpellWindow::onMouseWheel); - t->eventMouseButtonClick += MyGUI::newDelegate(this, &SpellWindow::onSpellSelected); - t->setStateSelected(*it == MWBase::Environment::get().getWindowManager()->getSelectedSpell()); - - // cost / success chance - MyGUI::Button* costChance = mSpellView->createWidget("SpellText", - MyGUI::IntCoord(4, mHeight, mWidth-8, spellHeight), MyGUI::Align::Left | MyGUI::Align::Top); - std::string cost = boost::lexical_cast(spell->mData.mCost); - std::string chance = boost::lexical_cast(int(MWMechanics::getSpellSuccessChance(*it, player))); - costChance->setCaption(cost + "/" + chance); - costChance->setTextAlign(MyGUI::Align::Right); - costChance->setNeedMouseFocus(false); - costChance->setStateSelected(*it == MWBase::Environment::get().getWindowManager()->getSelectedSpell()); - - t->setSize(mWidth-12-costChance->getTextSize().width, t->getHeight()); - - mHeight += spellHeight; - } - - - // enchanted items - addGroup("#{sMagicItem}", "#{sCostCharge}"); - - for (std::vector::const_iterator it = items.begin(); it != items.end(); ++it) - { - MWWorld::Ptr item = *it; - - const ESM::Enchantment* enchant = - esmStore.get().find(item.getClass().getEnchantment(item)); - - // check if the item is currently equipped (will display in a different color) - bool equipped = false; - for (int i=0; i < MWWorld::InventoryStore::Slots; ++i) - { - if (store.getSlot(i) != store.end() && *store.getSlot(i) == item) - { - equipped = true; - break; - } - } - - MyGUI::Button* t = mSpellView->createWidget(equipped ? "SpellText" : "SpellTextUnequipped", - MyGUI::IntCoord(4, mHeight, mWidth-8, spellHeight), MyGUI::Align::Left | MyGUI::Align::Top); - t->setCaption(item.getClass().getName(item)); - t->setTextAlign(MyGUI::Align::Left); - t->setUserData(item); - t->setUserString("ToolTipType", "ItemPtr"); - t->setUserString("Equipped", equipped ? "true" : "false"); - t->eventMouseButtonClick += MyGUI::newDelegate(this, &SpellWindow::onEnchantedItemSelected); - t->eventMouseWheel += MyGUI::newDelegate(this, &SpellWindow::onMouseWheel); - if (store.getSelectedEnchantItem() != store.end()) - t->setStateSelected(item == *store.getSelectedEnchantItem()); - - - // cost / charge - MyGUI::Button* costCharge = mSpellView->createWidget(equipped ? "SpellText" : "SpellTextUnequipped", - MyGUI::IntCoord(4, mHeight, mWidth-8, spellHeight), MyGUI::Align::Left | MyGUI::Align::Top); - - float enchantCost = enchant->mData.mCost; - int eSkill = player.getClass().getSkill(player, ESM::Skill::Enchant); - int castCost = std::max(1.f, enchantCost - (enchantCost / 100) * (eSkill - 10)); - - std::string cost = boost::lexical_cast(castCost); - int currentCharge = int(item.getCellRef().getEnchantmentCharge()); - if (currentCharge == -1) - currentCharge = enchant->mData.mCharge; - std::string charge = boost::lexical_cast(currentCharge); - if (enchant->mData.mType == ESM::Enchantment::CastOnce) - { - // this is Morrowind behaviour - cost = "100"; - charge = "100"; - } - - costCharge->setCaption(cost + "/" + charge); - costCharge->setTextAlign(MyGUI::Align::Right); - costCharge->setNeedMouseFocus(false); - if (store.getSelectedEnchantItem() != store.end()) - costCharge->setStateSelected(item == *store.getSelectedEnchantItem()); - - t->setSize(mWidth-12-costCharge->getTextSize().width, t->getHeight()); - - mHeight += spellHeight; - } - - mSpellView->setCanvasSize(mSpellView->getWidth(), std::max(mSpellView->getHeight(), mHeight)); + mSpellView->setModel(new SpellModel(MWBase::Environment::get().getWorld()->getPlayerPtr())); + mSpellView->update(); } - void SpellWindow::addGroup(const std::string &label, const std::string& label2) - { - if (mSpellView->getChildCount() > 0) - { - MyGUI::ImageBox* separator = mSpellView->createWidget("MW_HLine", - MyGUI::IntCoord(4, mHeight, mWidth-8, 18), - MyGUI::Align::Left | MyGUI::Align::Top); - separator->setNeedMouseFocus(false); - mHeight += 18; - } - - MyGUI::TextBox* groupWidget = mSpellView->createWidget("SandBrightText", - MyGUI::IntCoord(0, mHeight, mWidth, 24), - MyGUI::Align::Left | MyGUI::Align::Top | MyGUI::Align::HStretch); - groupWidget->setCaptionWithReplacing(label); - groupWidget->setTextAlign(MyGUI::Align::Left); - groupWidget->setNeedMouseFocus(false); - - if (label2 != "") - { - MyGUI::TextBox* groupWidget2 = mSpellView->createWidget("SandBrightText", - MyGUI::IntCoord(0, mHeight, mWidth-4, 24), - MyGUI::Align::Left | MyGUI::Align::Top); - groupWidget2->setCaptionWithReplacing(label2); - groupWidget2->setTextAlign(MyGUI::Align::Right); - groupWidget2->setNeedMouseFocus(false); - - groupWidget->setSize(mWidth-8-groupWidget2->getTextSize().width, groupWidget->getHeight()); - } - - mHeight += 24; - } - - void SpellWindow::onWindowResize(MyGUI::Window* _sender) - { - updateSpells(); - } - - void SpellWindow::onEnchantedItemSelected(MyGUI::Widget* _sender) + void SpellWindow::onEnchantedItemSelected(MWWorld::Ptr item, bool alreadyEquipped) { MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); - MWWorld::Ptr item = *_sender->getUserData(); // retrieve ContainerStoreIterator to the item MWWorld::ContainerStoreIterator it = store.begin(); @@ -317,73 +82,75 @@ namespace MWGui break; } } - assert(it != store.end()); + if (it == store.end()) + throw std::runtime_error("can't find selected item"); // equip, if it can be equipped and is not already equipped - if (_sender->getUserString("Equipped") == "false" + if (!alreadyEquipped && !item.getClass().getEquipmentSlots(item).first.empty()) { MWBase::Environment::get().getWindowManager()->getInventoryWindow()->useItem(item); + // make sure that item was successfully equipped + if (!store.isEquipped(item)) + return; } store.setSelectedEnchantItem(it); + // to reset WindowManager::mSelectedSpell immediately + MWBase::Environment::get().getWindowManager()->setSelectedEnchantItem(*it); updateSpells(); } - void SpellWindow::onSpellSelected(MyGUI::Widget* _sender) + void SpellWindow::askDeleteSpell(const std::string &spellId) { - std::string spellId = _sender->getUserString("Spell"); - MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); - MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); + // delete spell, if allowed + const ESM::Spell* spell = + MWBase::Environment::get().getWorld()->getStore().get().find(spellId); - if (MyGUI::InputManager::getInstance().isShiftPressed()) + if (spell->mData.mFlags & ESM::Spell::F_Always + || spell->mData.mType == ESM::Spell::ST_Power) { - // delete spell, if allowed - const ESM::Spell* spell = - MWBase::Environment::get().getWorld()->getStore().get().find(spellId); - - if (spell->mData.mFlags & ESM::Spell::F_Always - || spell->mData.mType == ESM::Spell::ST_Power) - { - MWBase::Environment::get().getWindowManager()->messageBox("#{sDeleteSpellError}"); - } - else - { - // ask for confirmation - mSpellToDelete = spellId; - ConfirmationDialog* dialog = MWBase::Environment::get().getWindowManager()->getConfirmationDialog(); - std::string question = MWBase::Environment::get().getWindowManager()->getGameSettingString("sQuestionDeleteSpell", "Delete %s?"); - question = boost::str(boost::format(question) % spell->mName); - dialog->open(question); - dialog->eventOkClicked.clear(); - dialog->eventOkClicked += MyGUI::newDelegate(this, &SpellWindow::onDeleteSpellAccept); - dialog->eventCancelClicked.clear(); - } + MWBase::Environment::get().getWindowManager()->messageBox("#{sDeleteSpellError}"); } else { - store.setSelectedEnchantItem(store.end()); - MWBase::Environment::get().getWindowManager()->setSelectedSpell(spellId, int(MWMechanics::getSpellSuccessChance(spellId, player))); + // ask for confirmation + mSpellToDelete = spellId; + ConfirmationDialog* dialog = MWBase::Environment::get().getWindowManager()->getConfirmationDialog(); + std::string question = MWBase::Environment::get().getWindowManager()->getGameSettingString("sQuestionDeleteSpell", "Delete %s?"); + question = boost::str(boost::format(question) % spell->mName); + dialog->open(question); + dialog->eventOkClicked.clear(); + dialog->eventOkClicked += MyGUI::newDelegate(this, &SpellWindow::onDeleteSpellAccept); + dialog->eventCancelClicked.clear(); } - - updateSpells(); } - int SpellWindow::estimateHeight(int numSpells) const + void SpellWindow::onModelIndexSelected(SpellModel::ModelIndex index) { - int height = 0; - height += 24 * 3 + 18 * 2; // group headings - height += numSpells * 18; - return height; + const Spell& spell = mSpellView->getModel()->getItem(index); + if (spell.mType == Spell::Type_EnchantedItem) + { + onEnchantedItemSelected(spell.mItem, spell.mActive); + } + else + { + if (MyGUI::InputManager::getInstance().isShiftPressed()) + askDeleteSpell(spell.mId); + else + onSpellSelected(spell.mId); + } } - void SpellWindow::onMouseWheel(MyGUI::Widget* _sender, int _rel) + void SpellWindow::onSpellSelected(const std::string& spellId) { - if (mSpellView->getViewOffset().top + _rel*0.3 > 0) - mSpellView->setViewOffset(MyGUI::IntPoint(0, 0)); - else - mSpellView->setViewOffset(MyGUI::IntPoint(0, mSpellView->getViewOffset().top + _rel*0.3)); + MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); + MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); + store.setSelectedEnchantItem(store.end()); + MWBase::Environment::get().getWindowManager()->setSelectedSpell(spellId, int(MWMechanics::getSpellSuccessChance(spellId, player))); + + updateSpells(); } void SpellWindow::onDeleteSpellAccept() @@ -399,4 +166,29 @@ namespace MWGui updateSpells(); } + + void SpellWindow::cycle(bool next) + { + mSpellView->setModel(new SpellModel(MWBase::Environment::get().getWorld()->getPlayerPtr())); + mSpellView->getModel()->update(); + + SpellModel::ModelIndex selected = 0; + for (SpellModel::ModelIndex i = 0; igetModel()->getItemCount()); ++i) + { + if (mSpellView->getModel()->getItem(i).mSelected) + selected = i; + } + + selected += next ? 1 : -1; + int itemcount = mSpellView->getModel()->getItemCount(); + if (itemcount == 0) + return; + selected = (selected + itemcount) % itemcount; + + const Spell& spell = mSpellView->getModel()->getItem(selected); + if (spell.mType == Spell::Type_EnchantedItem) + onEnchantedItemSelected(spell.mItem, spell.mActive); + else + onSpellSelected(spell.mId); + } } diff --git a/apps/openmw/mwgui/spellwindow.hpp b/apps/openmw/mwgui/spellwindow.hpp index 53eed1ba1..8b5474f58 100644 --- a/apps/openmw/mwgui/spellwindow.hpp +++ b/apps/openmw/mwgui/spellwindow.hpp @@ -4,13 +4,12 @@ #include "windowpinnablebase.hpp" #include "../mwworld/ptr.hpp" +#include "spellmodel.hpp" + namespace MWGui { class SpellIcons; - - bool sortItems(const MWWorld::Ptr& left, const MWWorld::Ptr& right); - - bool sortSpells(const std::string& left, const std::string& right); + class SpellView; class SpellWindow : public WindowPinnableBase, public NoDrop { @@ -22,28 +21,25 @@ namespace MWGui void onFrame(float dt) { NoDrop::onFrame(dt); } + /// Cycle to next/previous spell + void cycle(bool next); + protected: - MyGUI::ScrollView* mSpellView; MyGUI::Widget* mEffectBox; - int mHeight; - int mWidth; - std::string mSpellToDelete; - void addGroup(const std::string& label, const std::string& label2); - - int estimateHeight(int numSpells) const; - - void onWindowResize(MyGUI::Window* _sender); - void onEnchantedItemSelected(MyGUI::Widget* _sender); - void onSpellSelected(MyGUI::Widget* _sender); - void onMouseWheel(MyGUI::Widget* _sender, int _rel); + void onEnchantedItemSelected(MWWorld::Ptr item, bool alreadyEquipped); + void onSpellSelected(const std::string& spellId); + void onModelIndexSelected(SpellModel::ModelIndex index); void onDeleteSpellAccept(); + void askDeleteSpell(const std::string& spellId); virtual void onPinToggled(); + virtual void onTitleDoubleClicked(); virtual void open(); + SpellView* mSpellView; SpellIcons* mSpellIcons; }; } diff --git a/apps/openmw/mwgui/statswindow.cpp b/apps/openmw/mwgui/statswindow.cpp index b11258f1c..2a22f4239 100644 --- a/apps/openmw/mwgui/statswindow.cpp +++ b/apps/openmw/mwgui/statswindow.cpp @@ -1,6 +1,10 @@ #include "statswindow.hpp" -#include +#include +#include +#include +#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -8,6 +12,7 @@ #include "../mwworld/class.hpp" #include "../mwworld/player.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwmechanics/npcstats.hpp" @@ -66,7 +71,7 @@ namespace MWGui mSkillWidgetMap.insert(std::pair(i, (MyGUI::TextBox*)NULL)); } - MyGUI::WindowPtr t = static_cast(mMainWidget); + MyGUI::Window* t = mMainWidget->castType(); t->eventWindowChangeCoord += MyGUI::newDelegate(this, &StatsWindow::onWindowResize); } @@ -82,12 +87,15 @@ namespace MWGui { mLeftPane->setCoord( MyGUI::IntCoord(0, 0, 0.44*window->getSize().width, window->getSize().height) ); mRightPane->setCoord( MyGUI::IntCoord(0.44*window->getSize().width, 0, 0.56*window->getSize().width, window->getSize().height) ); + // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden + mSkillView->setVisibleVScroll(false); mSkillView->setCanvasSize (mSkillView->getWidth(), mSkillView->getCanvasSize().height); + mSkillView->setVisibleVScroll(true); } void StatsWindow::setBar(const std::string& name, const std::string& tname, int val, int max) { - MyGUI::ProgressPtr pt; + MyGUI::ProgressBar* pt; getWidget(pt, name); pt->setProgressRange(max); pt->setProgressPosition(val); @@ -99,8 +107,7 @@ namespace MWGui void StatsWindow::setPlayerName(const std::string& playerName) { - static_cast(mMainWidget)->setCaption(playerName); - adjustWindowCaption(); + mMainWidget->castType()->setCaption(playerName); } void StatsWindow::setValue (const std::string& id, const MWMechanics::AttributeValue& value) @@ -142,7 +149,7 @@ namespace MWGui // health, magicka, fatigue tooltip MyGUI::Widget* w; - std::string valStr = boost::lexical_cast(current) + "/" + boost::lexical_cast(modified); + std::string valStr = MyGUI::utility::toString(current) + "/" + MyGUI::utility::toString(modified); if (id == "HBar") { getWidget(w, "Health"); @@ -187,7 +194,7 @@ namespace MWGui if (widget) { int modified = value.getModified(), base = value.getBase(); - std::string text = boost::lexical_cast(modified); + std::string text = MyGUI::utility::toString(modified); std::string state = "normal"; if (modified > base) state = "increased"; @@ -236,10 +243,10 @@ namespace MWGui { int max = MWBase::Environment::get().getWorld()->getStore().get().find("iLevelUpTotal")->getInt(); getWidget(levelWidget, i==0 ? "Level_str" : "LevelText"); - levelWidget->setUserString("RangePosition_LevelProgress", boost::lexical_cast(PCstats.getLevelProgress())); - levelWidget->setUserString("Range_LevelProgress", boost::lexical_cast(max)); - levelWidget->setUserString("Caption_LevelProgressText", boost::lexical_cast(PCstats.getLevelProgress()) + "/" - + boost::lexical_cast(max)); + levelWidget->setUserString("RangePosition_LevelProgress", MyGUI::utility::toString(PCstats.getLevelProgress())); + levelWidget->setUserString("Range_LevelProgress", MyGUI::utility::toString(max)); + levelWidget->setUserString("Caption_LevelProgressText", MyGUI::utility::toString(PCstats.getLevelProgress()) + "/" + + MyGUI::utility::toString(max)); } setFactions(PCstats.getFactionRanks()); @@ -339,10 +346,14 @@ namespace MWGui { MyGUI::TextBox* skillNameWidget; - skillNameWidget = mSkillView->createWidget("SandText", coord1 + MyGUI::IntSize(coord2.width, 0), MyGUI::Align::Default); + skillNameWidget = mSkillView->createWidget("SandText", coord1, MyGUI::Align::Default); + skillNameWidget->setCaption(text); skillNameWidget->eventMouseWheel += MyGUI::newDelegate(this, &StatsWindow::onMouseWheel); + int textWidth = skillNameWidget->getTextSize().width; + skillNameWidget->setSize(textWidth, skillNameWidget->getHeight()); + mSkillWidgets.push_back(skillNameWidget); coord1.top += sLineHeight; @@ -365,18 +376,26 @@ namespace MWGui for (SkillList::const_iterator it = skills.begin(); it != end; ++it) { int skillId = *it; - if (skillId < 0 || skillId > ESM::Skill::Length) // Skip unknown skill indexes + if (skillId < 0 || skillId >= ESM::Skill::Length) // Skip unknown skill indexes continue; - assert(skillId >= 0 && skillId < ESM::Skill::Length); const std::string &skillNameId = ESM::Skill::sSkillNameIds[skillId]; const MWMechanics::SkillValue &stat = mSkillValues.find(skillId)->second; int base = stat.getBase(); int modified = stat.getModified(); - int progressPercent = stat.getProgress() * 100; + MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); const MWWorld::ESMStore &esmStore = MWBase::Environment::get().getWorld()->getStore(); + float progressRequirement = player.getClass().getNpcStats(player).getSkillProgressRequirement(skillId, + *esmStore.get().find(player.get()->mBase->mClass)); + + // This is how vanilla MW displays the progress bar (I think). Note it's slightly inaccurate, + // due to the int casting in the skill levelup logic. Also the progress label could in rare cases + // reach 100% without the skill levelling up. + // Leaving the original display logic for now, for consistency with ess-imported savegames. + int progressPercent = int(float(stat.getProgress()) / float(progressRequirement) * 100.f + 0.5f); + const ESM::Skill* skill = esmStore.get().find(skillId); std::string icon = "icons\\k\\" + ESM::Skill::sIconNames[skillId]; @@ -390,7 +409,7 @@ namespace MWGui else if (modified < base) state = "decreased"; MyGUI::TextBox* widget = addValueItem(MWBase::Environment::get().getWindowManager()->getGameSettingString(skillNameId, skillNameId), - boost::lexical_cast(static_cast(modified)), state, coord1, coord2); + MyGUI::utility::toString(static_cast(modified)), state, coord1, coord2); for (int i=0; i<2; ++i) { @@ -400,9 +419,24 @@ namespace MWGui mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("Caption_SkillDescription", skill->mDescription); mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("Caption_SkillAttribute", "#{sGoverningAttribute}: #{" + attr->mName + "}"); mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("ImageTexture_SkillImage", icon); - mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("Caption_SkillProgressText", boost::lexical_cast(progressPercent)+"/100"); - mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("Range_SkillProgress", "100"); - mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("RangePosition_SkillProgress", boost::lexical_cast(progressPercent)); + if (base < 100) + { + mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("Visible_SkillMaxed", "false"); + mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("UserData^Hidden_SkillMaxed", "true"); + + mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("Visible_SkillProgressVBox", "true"); + mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("UserData^Hidden_SkillProgressVBox", "false"); + + mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("Caption_SkillProgressText", MyGUI::utility::toString(progressPercent)+"/100"); + mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("Range_SkillProgress", "100"); + mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("RangePosition_SkillProgress", MyGUI::utility::toString(progressPercent)); + } else { + mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("Visible_SkillMaxed", "true"); + mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("UserData^Hidden_SkillMaxed", "false"); + + mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("Visible_SkillProgressVBox", "false"); + mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("UserData^Hidden_SkillProgressVBox", "true"); + } } mSkillWidgetMap[skillId] = widget; @@ -487,30 +521,30 @@ namespace MWGui std::string text; - text += std::string("#DDC79E") + faction->mName; + text += std::string("#{fontcolourhtml=header}") + faction->mName; if (expelled.find(it->first) != expelled.end()) - text += "\n#BF9959#{sExpelled}"; + text += "\n#{fontcolourhtml=normal}#{sExpelled}"; else { int rank = it->second; rank = std::max(0, std::min(9, rank)); - text += std::string("\n#BF9959") + faction->mRanks[rank]; + text += std::string("\n#{fontcolourhtml=normal}") + faction->mRanks[rank]; if (rank < 9) { // player doesn't have max rank yet - text += std::string("\n\n#DDC79E#{sNextRank} ") + faction->mRanks[rank+1]; + text += std::string("\n\n#{fontcolourhtml=header}#{sNextRank} ") + faction->mRanks[rank+1]; ESM::RankData rankData = faction->mData.mRankData[rank+1]; const ESM::Attribute* attr1 = store.get().find(faction->mData.mAttribute[0]); const ESM::Attribute* attr2 = store.get().find(faction->mData.mAttribute[1]); - text += "\n#BF9959#{" + attr1->mName + "}: " + boost::lexical_cast(rankData.mAttribute1) - + ", #{" + attr2->mName + "}: " + boost::lexical_cast(rankData.mAttribute2); + text += "\n#{fontcolourhtml=normal}#{" + attr1->mName + "}: " + MyGUI::utility::toString(rankData.mAttribute1) + + ", #{" + attr2->mName + "}: " + MyGUI::utility::toString(rankData.mAttribute2); - text += "\n\n#DDC79E#{sFavoriteSkills}"; - text += "\n#BF9959"; + text += "\n\n#{fontcolourhtml=header}#{sFavoriteSkills}"; + text += "\n#{fontcolourhtml=normal}"; bool firstSkill = true; for (int i=0; i<7; ++i) { @@ -528,9 +562,9 @@ namespace MWGui text += "\n"; if (rankData.mSkill1 > 0) - text += "\n#{sNeedOneSkill} " + boost::lexical_cast(rankData.mSkill1); + text += "\n#{sNeedOneSkill} " + MyGUI::utility::toString(rankData.mSkill1); if (rankData.mSkill2 > 0) - text += "\n#{sNeedTwoSkills} " + boost::lexical_cast(rankData.mSkill2); + text += "\n#{sNeedTwoSkills} " + MyGUI::utility::toString(rankData.mSkill2); } } @@ -559,7 +593,7 @@ namespace MWGui addSeparator(coord1, coord2); addValueItem(MWBase::Environment::get().getWindowManager()->getGameSettingString("sReputation", "Reputation"), - boost::lexical_cast(static_cast(mReputation)), "normal", coord1, coord2); + MyGUI::utility::toString(static_cast(mReputation)), "normal", coord1, coord2); for (int i=0; i<2; ++i) { @@ -569,7 +603,7 @@ namespace MWGui } addValueItem(MWBase::Environment::get().getWindowManager()->getGameSettingString("sBounty", "Bounty"), - boost::lexical_cast(static_cast(mBounty)), "normal", coord1, coord2); + MyGUI::utility::toString(static_cast(mBounty)), "normal", coord1, coord2); for (int i=0; i<2; ++i) { @@ -578,11 +612,20 @@ namespace MWGui mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("Caption_Text", "#{sCrimeHelp}"); } + // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden + mSkillView->setVisibleVScroll(false); mSkillView->setCanvasSize (mSkillView->getWidth(), std::max(mSkillView->getHeight(), coord1.top)); + mSkillView->setVisibleVScroll(true); } void StatsWindow::onPinToggled() { MWBase::Environment::get().getWindowManager()->setHMSVisibility(!mPinned); } + + void StatsWindow::onTitleDoubleClicked() + { + if (!mPinned) + MWBase::Environment::get().getWindowManager()->toggleVisible(GW_Stats); + } } diff --git a/apps/openmw/mwgui/statswindow.hpp b/apps/openmw/mwgui/statswindow.hpp index d90c16be9..2cf4ca819 100644 --- a/apps/openmw/mwgui/statswindow.hpp +++ b/apps/openmw/mwgui/statswindow.hpp @@ -1,11 +1,11 @@ #ifndef MWGUI_STATS_WINDOW_H #define MWGUI_STATS_WINDOW_H -#include "../mwworld/esmstore.hpp" - #include "../mwmechanics/stat.hpp" #include "windowpinnablebase.hpp" +#include + namespace MWGui { class WindowManager; @@ -37,7 +37,7 @@ namespace MWGui void setBounty (int bounty) { if (bounty != mBounty) mChanged = true; this->mBounty = bounty; } void updateSkillArea(); - virtual void open() { onWindowResize(static_cast(mMainWidget)); } + virtual void open() { onWindowResize(mMainWidget->castType()); } private: void addSkills(const SkillList &skills, const std::string &titleId, const std::string &titleDefault, MyGUI::IntCoord &coord1, MyGUI::IntCoord &coord2); @@ -74,6 +74,7 @@ namespace MWGui protected: virtual void onPinToggled(); + virtual void onTitleDoubleClicked(); }; } #endif diff --git a/apps/openmw/mwgui/textinput.cpp b/apps/openmw/mwgui/textinput.cpp index 954bc41ab..958b52dc0 100644 --- a/apps/openmw/mwgui/textinput.cpp +++ b/apps/openmw/mwgui/textinput.cpp @@ -3,6 +3,9 @@ #include "../mwbase/windowmanager.hpp" #include "../mwbase/environment.hpp" +#include +#include + namespace MWGui { @@ -61,13 +64,18 @@ namespace MWGui void TextInputDialog::onTextAccepted(MyGUI::Edit* _sender) { - if (mTextEdit->getCaption() == "") - { - MWBase::Environment::get().getWindowManager()->messageBox ("#{sNotifyMessage37}"); - MWBase::Environment::get().getWindowManager()->setKeyFocusWidget (mTextEdit); - } - else - eventDone(this); + onOkClicked(_sender); } + std::string TextInputDialog::getTextInput() const + { + return mTextEdit->getCaption(); + } + + void TextInputDialog::setTextInput(const std::string &text) + { + mTextEdit->setCaption(text); + } + + } diff --git a/apps/openmw/mwgui/textinput.hpp b/apps/openmw/mwgui/textinput.hpp index 1ed80fc1e..57dd34dd6 100644 --- a/apps/openmw/mwgui/textinput.hpp +++ b/apps/openmw/mwgui/textinput.hpp @@ -15,13 +15,18 @@ namespace MWGui public: TextInputDialog(); - std::string getTextInput() const { return mTextEdit->getCaption(); } - void setTextInput(const std::string &text) { mTextEdit->setCaption(text); } + std::string getTextInput() const; + void setTextInput(const std::string &text); void setNextButtonShow(bool shown); void setTextLabel(const std::string &label); virtual void open(); + /** Event : Dialog finished, OK button clicked.\n + signature : void method()\n + */ + EventHandle_WindowBase eventDone; + protected: void onOkClicked(MyGUI::Widget* _sender); void onTextAccepted(MyGUI::Edit* _sender); diff --git a/apps/openmw/mwgui/timeadvancer.cpp b/apps/openmw/mwgui/timeadvancer.cpp new file mode 100644 index 000000000..43504ce87 --- /dev/null +++ b/apps/openmw/mwgui/timeadvancer.cpp @@ -0,0 +1,73 @@ +#include "timeadvancer.hpp" + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" + +namespace MWGui +{ + TimeAdvancer::TimeAdvancer(float delay) + : mRunning(false), + mCurHour(0), + mHours(1), + mInterruptAt(-1), + mDelay(delay), + mRemainingTime(delay) + { + } + + void TimeAdvancer::run(int hours, int interruptAt) + { + mHours = hours; + mCurHour = 0; + mInterruptAt = interruptAt; + mRemainingTime = mDelay; + + mRunning = true; + } + + void TimeAdvancer::stop() + { + mRunning = false; + } + + void TimeAdvancer::onFrame(float dt) + { + if (!mRunning) + return; + + if (mCurHour == mInterruptAt) + { + stop(); + eventInterrupted(); + return; + } + + mRemainingTime -= dt; + + while (mRemainingTime <= 0) + { + mRemainingTime += mDelay; + ++mCurHour; + + if (mCurHour <= mHours) + eventProgressChanged(mCurHour, mHours); + else + { + stop(); + eventFinished(); + return; + } + + } + } + + int TimeAdvancer::getHours() + { + return mHours; + } + + bool TimeAdvancer::isRunning() + { + return mRunning; + } +} diff --git a/apps/openmw/mwgui/timeadvancer.hpp b/apps/openmw/mwgui/timeadvancer.hpp new file mode 100644 index 000000000..8367b5a8b --- /dev/null +++ b/apps/openmw/mwgui/timeadvancer.hpp @@ -0,0 +1,40 @@ +#ifndef MWGUI_TIMEADVANCER_H +#define MWGUI_TIMEADVANCER_H + +#include + +namespace MWGui +{ + class TimeAdvancer + { + public: + TimeAdvancer(float delay); + + void run(int hours, int interruptAt=-1); + void stop(); + void onFrame(float dt); + + int getHours(); + bool isRunning(); + + // signals + typedef MyGUI::delegates::CMultiDelegate0 EventHandle_Void; + typedef MyGUI::delegates::CMultiDelegate2 EventHandle_IntInt; + + EventHandle_IntInt eventProgressChanged; + EventHandle_Void eventInterrupted; + EventHandle_Void eventFinished; + + private: + bool mRunning; + + int mCurHour; + int mHours; + int mInterruptAt; + + float mDelay; + float mRemainingTime; + }; +} + +#endif diff --git a/apps/openmw/mwgui/tooltips.cpp b/apps/openmw/mwgui/tooltips.cpp index e09ff2487..2c10004a6 100644 --- a/apps/openmw/mwgui/tooltips.cpp +++ b/apps/openmw/mwgui/tooltips.cpp @@ -2,13 +2,22 @@ #include -#include +#include +#include +#include +#include + +#include +#include #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwbase/mechanicsmanager.hpp" #include "../mwworld/class.hpp" +#include "../mwworld/esmstore.hpp" +#include "../mwmechanics/spellcasting.hpp" #include "mapwindow.hpp" #include "inventorywindow.hpp" @@ -17,6 +26,7 @@ namespace MWGui { + std::string ToolTips::sSchoolNames[] = {"#{sSchoolAlteration}", "#{sSchoolConjuration}", "#{sSchoolDestruction}", "#{sSchoolIllusion}", "#{sSchoolMysticism}", "#{sSchoolRestoration}"}; ToolTips::ToolTips() : Layout("openmw_tooltips.layout") @@ -84,8 +94,6 @@ namespace MWGui || (MWBase::Environment::get().getWindowManager()->getMode() == GM_Container) || (MWBase::Environment::get().getWindowManager()->getMode() == GM_Inventory))) { - mFocusObject = MWBase::Environment::get().getWorld()->getFacedObject(); - if (mFocusObject.isEmpty ()) return; @@ -97,7 +105,9 @@ namespace MWGui setCoord(0, 0, 300, 300); mDynamicToolTipBox->setVisible(true); ToolTipInfo info; - info.caption=mFocusObject.getCellRef().getRefId(); + info.caption = mFocusObject.getClass().getName(mFocusObject); + if (info.caption.empty()) + info.caption=mFocusObject.getCellRef().getRefId(); info.icon=""; tooltipSize = createToolTip(info); } @@ -155,15 +165,19 @@ namespace MWGui // special handling for markers on the local map: the tooltip should only be visible // if the marker is not hidden due to the fog of war. - if (focus->getUserString ("IsMarker") == "true") + if (type == "MapMarker") { - LocalMapBase::MarkerPosition pos = *focus->getUserData(); + LocalMapBase::MarkerUserData data = *focus->getUserData(); - if (!MWBase::Environment::get().getWorld ()->isPositionExplored (pos.nX, pos.nY, pos.cellX, pos.cellY, pos.interior)) + if (!MWBase::Environment::get().getWorld ()->isPositionExplored (data.nX, data.nY, data.cellX, data.cellY, data.interior)) return; - } - if (type == "ItemPtr") + ToolTipInfo info; + info.text = data.caption; + info.notes = data.notes; + tooltipSize = createToolTip(info); + } + else if (type == "ItemPtr") { mFocusObject = *focus->getUserData(); tooltipSize = getToolTipViaPtr(false); @@ -218,6 +232,12 @@ namespace MWGui params.mNoTarget = false; effects.push_back(params); } + if (MWMechanics::spellIncreasesSkill(spell)) // display school of spells that contribute to skill progress + { + MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); + int school = MWMechanics::getSpellSchool(spell, player); + info.text = "#{sSchool}: " + sSchoolNames[school]; + } info.effects = effects; tooltipSize = createToolTip(info); } @@ -236,12 +256,23 @@ namespace MWGui size_t underscorePos = it->first.find("_"); if (underscorePos == std::string::npos) continue; - std::string propertyKey = it->first.substr(0, underscorePos); + std::string key = it->first.substr(0, underscorePos); std::string widgetName = it->first.substr(underscorePos+1, it->first.size()-(underscorePos+1)); + std::string type = "Property"; + size_t caretPos = key.find("^"); + if (caretPos != std::string::npos) + { + type = key.substr(0, caretPos); + key.erase(key.begin(), key.begin() + caretPos + 1); + } + MyGUI::Widget* w; getWidget(w, widgetName); - w->setProperty(propertyKey, it->second); + if (type == "Property") + w->setProperty(key, it->second); + else if (type == "UserData") + w->setUserString(key, it->second); } tooltipSize = tooltip->getSize(); @@ -319,20 +350,6 @@ namespace MWGui return tooltipSize; } - void ToolTips::findImageExtension(std::string& image) - { - int len = image.size(); - if (len < 4) return; - - if (!Ogre::ResourceGroupManager::getSingleton().resourceExists(Ogre::ResourceGroupManager::AUTODETECT_RESOURCE_GROUP_NAME, image)) - { - // Change texture extension to .dds - image[len-3] = 'd'; - image[len-2] = 'd'; - image[len-1] = 's'; - } - } - MyGUI::IntSize ToolTips::createToolTip(const MWGui::ToolTipInfo& info) { mDynamicToolTipBox->setVisible(true); @@ -371,8 +388,7 @@ namespace MWGui const int imageCaptionHPadding = (caption != "" ? 8 : 0); const int imageCaptionVPadding = (caption != "" ? 4 : 0); - std::string realImage = "icons\\" + image; - findImageExtension(realImage); + std::string realImage = Misc::ResourceHelpers::correctIconPath(image); MyGUI::EditBox* captionWidget = mDynamicToolTipBox->createWidget("NormalText", MyGUI::IntCoord(0, 0, 300, 300), MyGUI::Align::Left | MyGUI::Align::Top, "ToolTipCaption"); captionWidget->setProperty("Static", "true"); @@ -393,16 +409,33 @@ namespace MWGui MyGUI::IntSize totalSize = MyGUI::IntSize( std::min(std::max(textSize.width,captionSize.width + ((image != "") ? imageCaptionHPadding : 0)),maximumWidth), ((text != "") ? textSize.height + imageCaptionVPadding : 0) + captionHeight ); + for (std::vector::const_iterator it = info.notes.begin(); it != info.notes.end(); ++it) + { + MyGUI::ImageBox* icon = mDynamicToolTipBox->createWidget("MarkerButton", + MyGUI::IntCoord(padding.left, totalSize.height+padding.top, 8, 8), MyGUI::Align::Default); + icon->setColour(MyGUI::Colour(1.0,0.3,0.3)); + MyGUI::EditBox* edit = mDynamicToolTipBox->createWidget("SandText", + MyGUI::IntCoord(padding.left+8+4, totalSize.height+padding.top, 300-padding.left-8-4, 300-totalSize.height), + MyGUI::Align::Default); + edit->setEditMultiLine(true); + edit->setEditWordWrap(true); + edit->setCaption(*it); + edit->setSize(edit->getWidth(), edit->getTextSize().height); + icon->setPosition(icon->getLeft(),(edit->getTop()+edit->getBottom())/2-icon->getHeight()/2); + totalSize.height += std::max(edit->getHeight(), icon->getHeight()); + totalSize.width = std::max(totalSize.width, edit->getWidth()+8+4); + } + if (!info.effects.empty()) { MyGUI::Widget* effectArea = mDynamicToolTipBox->createWidget("", MyGUI::IntCoord(padding.left, totalSize.height, 300-padding.left, 300-totalSize.height), - MyGUI::Align::Stretch, "ToolTipEffectArea"); + MyGUI::Align::Stretch); MyGUI::IntCoord coord(0, 6, totalSize.width, 24); Widgets::MWEffectListPtr effectsWidget = effectArea->createWidget - ("MW_StatName", coord, MyGUI::Align::Default, "ToolTipEffectsWidget"); + ("MW_StatName", coord, MyGUI::Align::Default); effectsWidget->setEffectList(info.effects); std::vector effectItems; @@ -416,12 +449,12 @@ namespace MWGui assert(enchant); MyGUI::Widget* enchantArea = mDynamicToolTipBox->createWidget("", MyGUI::IntCoord(padding.left, totalSize.height, 300-padding.left, 300-totalSize.height), - MyGUI::Align::Stretch, "ToolTipEnchantArea"); + MyGUI::Align::Stretch); MyGUI::IntCoord coord(0, 6, totalSize.width, 24); Widgets::MWEffectListPtr enchantWidget = enchantArea->createWidget - ("MW_StatName", coord, MyGUI::Align::Default, "ToolTipEnchantWidget"); + ("MW_StatName", coord, MyGUI::Align::Default); enchantWidget->setEffectList(Widgets::MWEffectList::effectListFromESM(&enchant->mEffects)); std::vector enchantEffectItems; @@ -460,7 +493,7 @@ namespace MWGui chargeCoord = MyGUI::IntCoord((totalSize.width - chargeAndTextWidth)/2 + chargeTextWidth, coord.top+6, chargeWidth, 18); } Widgets::MWDynamicStatPtr chargeWidget = enchantArea->createWidget - ("MW_ChargeBar", chargeCoord, MyGUI::Align::Default, "ToolTipEnchantCharge"); + ("MW_ChargeBar", chargeCoord, MyGUI::Align::Default); chargeWidget->setValue(charge, maxCharge); totalSize.height += 24; } @@ -495,7 +528,7 @@ namespace MWGui { MyGUI::ImageBox* imageWidget = mDynamicToolTipBox->createWidget("ImageBox", MyGUI::IntCoord((totalSize.width - captionSize.width - imageCaptionHPadding)/2, 0, imageSize, imageSize), - MyGUI::Align::Left | MyGUI::Align::Top, "ToolTipImage"); + MyGUI::Align::Left | MyGUI::Align::Top); imageWidget->setImageTexture(realImage); imageWidget->setPosition (imageWidget->getPosition() + padding); } @@ -544,7 +577,7 @@ namespace MWGui if (value == 1) return ""; else - return " (" + boost::lexical_cast(value) + ")"; + return " (" + MyGUI::utility::toString(value) + ")"; } std::string ToolTips::getCellRefString(const MWWorld::CellRef& cellref) @@ -554,6 +587,15 @@ namespace MWGui ret += getMiscString(cellref.getFaction(), "Faction"); if (cellref.getFactionRank() > 0) ret += getValueString(cellref.getFactionRank(), "Rank"); + + std::vector > itemOwners = + MWBase::Environment::get().getMechanicsManager()->getStolenItemOwners(cellref.getRefId()); + + for (std::vector >::const_iterator it = itemOwners.begin(); it != itemOwners.end(); ++it) + { + ret += std::string("\nStolen from ") + it->first; + } + ret += getMiscString(cellref.getGlobalVariable(), "Global"); return ret; } @@ -627,8 +669,8 @@ namespace MWGui MWWorld::Store::iterator it = skills.begin(); for (; it != skills.end(); ++it) { - if (it->mData.mSpecialization == specId) - specText += std::string("\n#{") + ESM::Skill::sSkillNameIds[it->mIndex] + "}"; + if (it->second.mData.mSpecialization == specId) + specText += std::string("\n#{") + ESM::Skill::sSkillNameIds[it->first] + "}"; } widget->setUserString("Caption_CenteredCaptionText", specText); widget->setUserString("ToolTipLayout", "TextWithCenteredCaptionToolTip"); @@ -644,13 +686,11 @@ namespace MWGui widget->setUserString("ToolTipType", "Layout"); widget->setUserString("ToolTipLayout", "BirthSignToolTip"); - std::string image = sign->mTexture; - image.replace(image.size()-3, 3, "dds"); - widget->setUserString("ImageTexture_BirthSignImage", "textures\\" + image); + widget->setUserString("ImageTexture_BirthSignImage", Misc::ResourceHelpers::correctTexturePath(sign->mTexture)); std::string text; text += sign->mName; - text += "\n#BF9959" + sign->mDescription; + text += "\n#{fontcolourhtml=normal}" + sign->mDescription; std::vector abilities, powers, spells; @@ -690,13 +730,13 @@ namespace MWGui { if (it == categories[category].spells.begin()) { - text += std::string("\n#DDC79E") + std::string("#{") + categories[category].label + "}"; + text += std::string("\n#{fontcolourhtml=header}") + std::string("#{") + categories[category].label + "}"; } const std::string &spellId = *it; const ESM::Spell *spell = store.get().find(spellId); - text += "\n#BF9959" + spell->mName; + text += "\n#{fontcolourhtml=normal}" + spell->mName; } } @@ -739,29 +779,15 @@ namespace MWGui const std::string &name = ESM::MagicEffect::effectIdToString (id); std::string icon = effect->mIcon; - - int slashPos = icon.find("\\"); + int slashPos = icon.rfind('\\'); icon.insert(slashPos+1, "b_"); - - icon[icon.size()-3] = 'd'; - icon[icon.size()-2] = 'd'; - icon[icon.size()-1] = 's'; - - icon = "icons\\" + icon; - - std::vector schools; - schools.push_back ("#{sSchoolAlteration}"); - schools.push_back ("#{sSchoolConjuration}"); - schools.push_back ("#{sSchoolDestruction}"); - schools.push_back ("#{sSchoolIllusion}"); - schools.push_back ("#{sSchoolMysticism}"); - schools.push_back ("#{sSchoolRestoration}"); + icon = Misc::ResourceHelpers::correctIconPath(icon); widget->setUserString("ToolTipType", "Layout"); widget->setUserString("ToolTipLayout", "MagicEffectToolTip"); widget->setUserString("Caption_MagicEffectName", "#{" + name + "}"); widget->setUserString("Caption_MagicEffectDescription", effect->mDescription); - widget->setUserString("Caption_MagicEffectSchool", "#{sSchool}: " + schools[effect->mData.mSchool]); + widget->setUserString("Caption_MagicEffectSchool", "#{sSchool}: " + sSchoolNames[effect->mData.mSchool]); widget->setUserString("ImageTexture_MagicEffectImage", icon); } diff --git a/apps/openmw/mwgui/tooltips.hpp b/apps/openmw/mwgui/tooltips.hpp index 8b6174b87..4bd4d88aa 100644 --- a/apps/openmw/mwgui/tooltips.hpp +++ b/apps/openmw/mwgui/tooltips.hpp @@ -7,6 +7,12 @@ #include "widgets.hpp" +namespace ESM +{ + class Class; + struct Race; +} + namespace MWGui { // Info about tooltip that is supplied by the MWWorld::Class object @@ -32,6 +38,9 @@ namespace MWGui // effects (for potions, ingredients) Widgets::SpellEffectList effects; + // local map notes + std::vector notes; + bool isPotion; // potions do not show target in the tooltip bool wordWrap; }; @@ -84,8 +93,6 @@ namespace MWGui MWWorld::Ptr mFocusObject; - void findImageExtension(std::string& image); - MyGUI::IntSize getToolTipViaPtr (bool image=true); ///< @return requested tooltip size @@ -98,6 +105,8 @@ namespace MWGui /// Adjust position for a tooltip so that it doesn't leave the screen and does not obscure the mouse cursor void position(MyGUI::IntPoint& position, MyGUI::IntSize size, MyGUI::IntSize viewportSize); + static std::string sSchoolNames[6]; + int mHorizontalScrollIndex; diff --git a/apps/openmw/mwgui/tradeitemmodel.cpp b/apps/openmw/mwgui/tradeitemmodel.cpp index 5fdf604da..a5a2b3e88 100644 --- a/apps/openmw/mwgui/tradeitemmodel.cpp +++ b/apps/openmw/mwgui/tradeitemmodel.cpp @@ -93,6 +93,21 @@ namespace MWGui unborrowImpl(item, count, mBorrowedFromUs); } + void TradeItemModel::adjustEncumbrance(float &encumbrance) + { + for (std::vector::iterator it = mBorrowedToUs.begin(); it != mBorrowedToUs.end(); ++it) + { + MWWorld::Ptr item = it->mBase; + encumbrance += item.getClass().getWeight(item) * it->mCount; + } + for (std::vector::iterator it = mBorrowedFromUs.begin(); it != mBorrowedFromUs.end(); ++it) + { + MWWorld::Ptr item = it->mBase; + encumbrance -= item.getClass().getWeight(item) * it->mCount; + } + encumbrance = std::max(0.f, encumbrance); + } + void TradeItemModel::abort() { mBorrowedFromUs.clear(); @@ -120,11 +135,9 @@ namespace MWGui if (i == sourceModel->getItemCount()) throw std::runtime_error("The borrowed item disappeared"); - // reset owner while copying, but only for items bought by the player - bool setNewOwner = (mMerchant.isEmpty()); const ItemStack& item = sourceModel->getItem(i); // copy the borrowed items to our model - copyItem(item, it->mCount, setNewOwner); + copyItem(item, it->mCount); // then remove them from the source model sourceModel->removeItem(item, it->mCount); } @@ -154,26 +167,14 @@ namespace MWGui continue; // Bound items may not be bought - if (item.mBase.getCellRef().getRefId().size() > 6 - && item.mBase.getCellRef().getRefId().substr(0,6) == "bound_") - { + if (item.mFlags & ItemStack::Flag_Bound) continue; - } // don't show equipped items if(mMerchant.getClass().hasInventoryStore(mMerchant)) { - bool isEquipped = false; MWWorld::InventoryStore& store = mMerchant.getClass().getInventoryStore(mMerchant); - for (int slot=0; slot getItemsBorrowedToUs(); private: diff --git a/apps/openmw/mwgui/tradewindow.cpp b/apps/openmw/mwgui/tradewindow.cpp index c56c2ee94..40cf3e9bf 100644 --- a/apps/openmw/mwgui/tradewindow.cpp +++ b/apps/openmw/mwgui/tradewindow.cpp @@ -1,6 +1,10 @@ #include "tradewindow.hpp" -#include +#include +#include +#include + +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -12,6 +16,7 @@ #include "../mwworld/manualref.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwmechanics/creaturestats.hpp" @@ -22,6 +27,7 @@ #include "tradeitemmodel.hpp" #include "countdialog.hpp" #include "dialogue.hpp" +#include "controllers.hpp" namespace { @@ -44,8 +50,6 @@ namespace MWGui TradeWindow::TradeWindow() : WindowBase("openmw_trade_window.layout") , mCurrentBalance(0) - , mBalanceButtonsState(BBS_None) - , mBalanceChangePause(0.0) , mItemToSell(-1) , mTradeModel(NULL) , mSortModel(NULL) @@ -87,11 +91,26 @@ namespace MWGui mDecreaseButton->eventMouseButtonPressed += MyGUI::newDelegate(this, &TradeWindow::onDecreaseButtonPressed); mDecreaseButton->eventMouseButtonReleased += MyGUI::newDelegate(this, &TradeWindow::onBalanceButtonReleased); - mTotalBalance->eventEditTextChange += MyGUI::newDelegate(this, &TradeWindow::onBalanceEdited); + mTotalBalance->eventValueChanged += MyGUI::newDelegate(this, &TradeWindow::onBalanceValueChanged); + mTotalBalance->setMinValue(INT_MIN+1); // disallow INT_MIN since abs(INT_MIN) is undefined setCoord(400, 0, 400, 300); } + void TradeWindow::restock() + { + // Restock items on the actor inventory + mPtr.getClass().restock(mPtr); + + // Also restock any containers owned by this merchant, which are also available to buy in the trade window + std::vector itemSources; + MWBase::Environment::get().getWorld()->getContainersOwnedBy(mPtr, itemSources); + for (std::vector::iterator it = itemSources.begin(); it != itemSources.end(); ++it) + { + it->getClass().restock(*it); + } + } + void TradeWindow::startTrade(const MWWorld::Ptr& actor) { mPtr = actor; @@ -115,8 +134,6 @@ namespace MWGui updateLabels(); - // Careful here. setTitle may cause size updates, causing itemview redraw, so make sure to do it last - // or we end up using a possibly invalid model. setTitle(actor.getClass().getName(actor)); onFilterChanged(mFilterAll); @@ -141,7 +158,7 @@ namespace MWGui mFilterMagic->setStateSelected(false); mFilterMisc->setStateSelected(false); - static_cast(_sender)->setStateSelected(true); + _sender->castType()->setStateSelected(true); mItemView->update(); } @@ -242,21 +259,6 @@ namespace MWGui } } - void TradeWindow::onFrame(float frameDuration) - { - if (!mMainWidget->getVisible() || mBalanceButtonsState == BBS_None) - return; - - mBalanceChangePause -= frameDuration; - if (mBalanceChangePause < 0.0) { - mBalanceChangePause += sBalanceChangeInterval; - if (mBalanceButtonsState == BBS_Increase) - onIncreaseButtonTriggered(); - else if (mBalanceButtonsState == BBS_Decrease) - onDecreaseButtonTriggered(); - } - } - void TradeWindow::onOfferButtonClicked(MyGUI::Widget* _sender) { TradeItemModel* playerItemModel = MWBase::Environment::get().getWindowManager()->getInventoryWindow()->getTradeModel(); @@ -299,22 +301,24 @@ namespace MWGui // check if the player is attempting to sell back an item stolen from this actor for (std::vector::iterator it = merchantBought.begin(); it != merchantBought.end(); ++it) { - if (Misc::StringUtils::ciEqual(it->mBase.getCellRef().getOwner(), mPtr.getCellRef().getRefId())) + if (MWBase::Environment::get().getMechanicsManager()->isItemStolenFrom(it->mBase.getCellRef().getRefId(), + mPtr.getCellRef().getRefId())) { std::string msg = gmst.find("sNotifyMessage49")->getString(); if (msg.find("%s") != std::string::npos) msg.replace(msg.find("%s"), 2, it->mBase.getClass().getName(it->mBase)); MWBase::Environment::get().getWindowManager()->messageBox(msg); - MWBase::Environment::get().getDialogueManager()->say(mPtr, "Thief"); - MWBase::Environment::get().getMechanicsManager()->reportCrime(player, mPtr, MWBase::MechanicsManager::OT_Theft, + MWBase::Environment::get().getMechanicsManager()->commitCrime(player, mPtr, MWBase::MechanicsManager::OT_Theft, it->mBase.getClass().getValue(it->mBase) - * it->mCount); + * it->mCount, true); onCancelButtonClicked(mCancelButton); - MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Dialogue); + MWBase::Environment::get().getDialogueManager()->goodbyeSelected(); return; } } + // TODO: move to mwmechanics + // Is the player buying? bool buying = (mCurrentMerchantOffer < 0); @@ -342,14 +346,15 @@ namespace MWGui const MWMechanics::CreatureStats &sellerStats = mPtr.getClass().getCreatureStats(mPtr); const MWMechanics::CreatureStats &playerStats = player.getClass().getCreatureStats(player); - float a1 = std::min(player.getClass().getSkill(player, ESM::Skill::Mercantile), 100); - float b1 = std::min(0.1f * playerStats.getAttribute(ESM::Attribute::Luck).getModified(), 10.f); - float c1 = std::min(0.2f * playerStats.getAttribute(ESM::Attribute::Personality).getModified(), 10.f); - float d1 = std::min(mPtr.getClass().getSkill(mPtr, ESM::Skill::Mercantile), 100); - float e1 = std::min(0.1f * sellerStats.getAttribute(ESM::Attribute::Luck).getModified(), 10.f); - float f1 = std::min(0.2f * sellerStats.getAttribute(ESM::Attribute::Personality).getModified(), 10.f); + float a1 = player.getClass().getSkill(player, ESM::Skill::Mercantile); + float b1 = 0.1f * playerStats.getAttribute(ESM::Attribute::Luck).getModified(); + float c1 = 0.2f * playerStats.getAttribute(ESM::Attribute::Personality).getModified(); + float d1 = mPtr.getClass().getSkill(mPtr, ESM::Skill::Mercantile); + float e1 = 0.1f * sellerStats.getAttribute(ESM::Attribute::Luck).getModified(); + float f1 = 0.2f * sellerStats.getAttribute(ESM::Attribute::Personality).getModified(); - float pcTerm = (clampedDisposition - 50 + a1 + b1 + c1) * playerStats.getFatigueTerm(); + float dispositionTerm = gmst.find("fDispositionMod")->getFloat() * (clampedDisposition - 50); + float pcTerm = (dispositionTerm - 50 + a1 + b1 + c1) * playerStats.getFatigueTerm(); float npcTerm = (d1 + e1 + f1) * sellerStats.getFatigueTerm(); float x = gmst.find("fBargainOfferMulti")->getFloat() * d + gmst.find("fBargainOfferBase")->getFloat(); if (buying) @@ -370,7 +375,15 @@ namespace MWGui } //skill use! - player.getClass().skillUsageSucceeded(player, ESM::Skill::Mercantile, 0); + float skillGain = 0.f; + int finalPrice = std::abs(mCurrentBalance); + int initialMerchantOffer = std::abs(mCurrentMerchantOffer); + if (!buying && (finalPrice > initialMerchantOffer) && finalPrice > 0) + skillGain = int(100 * (finalPrice - initialMerchantOffer) / float(finalPrice)); + else if (buying && (finalPrice < initialMerchantOffer) && initialMerchantOffer > 0) + skillGain = int(100 * (initialMerchantOffer - finalPrice) / float(initialMerchantOffer)); + + player.getClass().skillUsageSucceeded(player, ESM::Skill::Mercantile, 0, skillGain); } int iBarterSuccessDisposition = gmst.find("iBarterSuccessDisposition")->getInt(); @@ -409,40 +422,55 @@ namespace MWGui updateLabels(); } + void TradeWindow::addRepeatController(MyGUI::Widget *widget) + { + MyGUI::ControllerItem* item = MyGUI::ControllerManager::getInstance().createItem(Controllers::ControllerRepeatEvent::getClassTypeName()); + Controllers::ControllerRepeatEvent* controller = item->castType(); + controller->eventRepeatClick += MyGUI::newDelegate(this, &TradeWindow::onRepeatClick); + controller->setRepeat(sBalanceChangeInitialPause, sBalanceChangeInterval); + MyGUI::ControllerManager::getInstance().addItem(widget, controller); + } + void TradeWindow::onIncreaseButtonPressed(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id) { - mBalanceButtonsState = BBS_Increase; - mBalanceChangePause = sBalanceChangeInitialPause; + addRepeatController(_sender); onIncreaseButtonTriggered(); } void TradeWindow::onDecreaseButtonPressed(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id) { - mBalanceButtonsState = BBS_Decrease; - mBalanceChangePause = sBalanceChangeInitialPause; + addRepeatController(_sender); onDecreaseButtonTriggered(); } + void TradeWindow::onRepeatClick(MyGUI::Widget* widget, MyGUI::ControllerItem* controller) + { + if (widget == mIncreaseButton) + onIncreaseButtonTriggered(); + else if (widget == mDecreaseButton) + onDecreaseButtonTriggered(); + } + void TradeWindow::onBalanceButtonReleased(MyGUI::Widget *_sender, int _left, int _top, MyGUI::MouseButton _id) { - mBalanceButtonsState = BBS_None; + MyGUI::ControllerManager::getInstance().removeItem(_sender); } - void TradeWindow::onBalanceEdited(MyGUI::EditBox *_sender) + void TradeWindow::onBalanceValueChanged(int value) { - try - { - unsigned int count = boost::lexical_cast(_sender->getCaption()); - mCurrentBalance = count * (mCurrentBalance >= 0 ? 1 : -1); - updateLabels(); - } - catch (std::bad_cast&) - { - } + // Entering a "-" sign inverts the buying/selling state + mCurrentBalance = (mCurrentBalance >= 0 ? 1 : -1) * value; + updateLabels(); + + if (value != std::abs(value)) + mTotalBalance->setValue(std::abs(value)); } void TradeWindow::onIncreaseButtonTriggered() { + // prevent overflows, and prevent entering INT_MIN since abs(INT_MIN) is undefined + if (mCurrentBalance == INT_MAX || mCurrentBalance == INT_MIN+1) + return; if(mCurrentBalance<=-1) mCurrentBalance -= 1; if(mCurrentBalance>=1) mCurrentBalance += 1; updateLabels(); @@ -460,40 +488,54 @@ namespace MWGui MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); int playerGold = player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId); - mPlayerGold->setCaptionWithReplacing("#{sYourGold} " + boost::lexical_cast(playerGold)); + mPlayerGold->setCaptionWithReplacing("#{sYourGold} " + MyGUI::utility::toString(playerGold)); if (mCurrentBalance > 0) { mTotalBalanceLabel->setCaptionWithReplacing("#{sTotalSold}"); - mTotalBalance->setCaption(boost::lexical_cast(mCurrentBalance)); } else { mTotalBalanceLabel->setCaptionWithReplacing("#{sTotalCost}"); - mTotalBalance->setCaption(boost::lexical_cast(-mCurrentBalance)); } - mMerchantGold->setCaptionWithReplacing("#{sSellerGold} " + boost::lexical_cast(getMerchantGold())); + mTotalBalance->setValue(std::abs(mCurrentBalance)); + + mMerchantGold->setCaptionWithReplacing("#{sSellerGold} " + MyGUI::utility::toString(getMerchantGold())); } - void TradeWindow::sellToNpc(const MWWorld::Ptr& item, int count, bool boughtItem) + void TradeWindow::updateOffer() { - int diff = MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mPtr, getEffectiveValue(item, count), boughtItem); + TradeItemModel* playerTradeModel = MWBase::Environment::get().getWindowManager()->getInventoryWindow()->getTradeModel(); - mCurrentBalance += diff; - mCurrentMerchantOffer += diff; + int merchantOffer = 0; + + std::vector playerBorrowed = playerTradeModel->getItemsBorrowedToUs(); + for (std::vector::const_iterator it = playerBorrowed.begin(); it != playerBorrowed.end(); ++it) + { + merchantOffer -= MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mPtr, getEffectiveValue(it->mBase, it->mCount), true); + } + std::vector merchantBorrowed = mTradeModel->getItemsBorrowedToUs(); + for (std::vector::const_iterator it = merchantBorrowed.begin(); it != merchantBorrowed.end(); ++it) + { + merchantOffer += MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mPtr, getEffectiveValue(it->mBase, it->mCount), false); + } + + int diff = merchantOffer - mCurrentMerchantOffer; + mCurrentMerchantOffer = merchantOffer; + mCurrentBalance += diff; updateLabels(); } - void TradeWindow::buyFromNpc(const MWWorld::Ptr& item, int count, bool soldItem) + void TradeWindow::sellToNpc(const MWWorld::Ptr& item, int count, bool boughtItem) { - int diff = MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mPtr, getEffectiveValue(item, count), !soldItem); - - mCurrentBalance -= diff; - mCurrentMerchantOffer -= diff; + updateOffer(); + } - updateLabels(); + void TradeWindow::buyFromNpc(const MWWorld::Ptr& item, int count, bool soldItem) + { + updateOffer(); } void TradeWindow::onReferenceUnavailable() @@ -509,29 +551,6 @@ namespace MWGui return merchantGold; } - void TradeWindow::restock() - { - MWMechanics::CreatureStats &sellerStats = mPtr.getClass().getCreatureStats(mPtr); - float delay = MWBase::Environment::get().getWorld()->getStore().get().find("fBarterGoldResetDelay")->getFloat(); - - if (MWBase::Environment::get().getWorld()->getTimeStamp() >= sellerStats.getLastRestockTime() + delay) - { - sellerStats.setGoldPool(mPtr.getClass().getBaseGold(mPtr)); - - mPtr.getClass().restock(mPtr); - - // Also restock any containers owned by this merchant, which are also available to buy in the trade window - std::vector itemSources; - MWBase::Environment::get().getWorld()->getContainersOwnedBy(mPtr, itemSources); - for (std::vector::iterator it = itemSources.begin(); it != itemSources.end(); ++it) - { - it->getClass().restock(*it); - } - - sellerStats.setLastRestockTime(MWBase::Environment::get().getWorld()->getTimeStamp()); - } - } - void TradeWindow::resetReference() { ReferenceInterface::resetReference(); diff --git a/apps/openmw/mwgui/tradewindow.hpp b/apps/openmw/mwgui/tradewindow.hpp index b487a8870..a23196d70 100644 --- a/apps/openmw/mwgui/tradewindow.hpp +++ b/apps/openmw/mwgui/tradewindow.hpp @@ -1,20 +1,19 @@ #ifndef MWGUI_TRADEWINDOW_H #define MWGUI_TRADEWINDOW_H -#include "container.hpp" +#include "referenceinterface.hpp" +#include "windowbase.hpp" -namespace MyGUI +namespace Gui { - class Gui; - class Widget; + class NumericEditBox; } -namespace MWGui +namespace MyGUI { - class WindowManager; + class ControllerItem; } - namespace MWGui { class ItemView; @@ -28,8 +27,6 @@ namespace MWGui void startTrade(const MWWorld::Ptr& actor); - void onFrame(float frameDuration); - void borrowItem (int index, size_t count); void returnItem (int index, size_t count); @@ -56,7 +53,7 @@ namespace MWGui MyGUI::Button* mIncreaseButton; MyGUI::Button* mDecreaseButton; MyGUI::TextBox* mTotalBalanceLabel; - MyGUI::EditBox* mTotalBalance; + Gui::NumericEditBox* mTotalBalance; MyGUI::Widget* mBottomPane; @@ -71,17 +68,11 @@ namespace MWGui int mCurrentBalance; int mCurrentMerchantOffer; - enum BalanceButtonsState { - BBS_None, - BBS_Increase, - BBS_Decrease - } mBalanceButtonsState; - /// pause before next balance change will trigger while user holds +/- button pressed - float mBalanceChangePause; - void sellToNpc(const MWWorld::Ptr& item, int count, bool boughtItem); ///< only used for adjusting the gold balance void buyFromNpc(const MWWorld::Ptr& item, int count, bool soldItem); ///< only used for adjusting the gold balance + void updateOffer(); + void onItemSelected (int index); void sellItem (MyGUI::Widget* sender, int count); @@ -92,7 +83,10 @@ namespace MWGui void onIncreaseButtonPressed(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id); void onDecreaseButtonPressed(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id); void onBalanceButtonReleased(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id); - void onBalanceEdited(MyGUI::EditBox* _sender); + void onBalanceValueChanged(int value); + void onRepeatClick(MyGUI::Widget* widget, MyGUI::ControllerItem* controller); + + void addRepeatController(MyGUI::Widget* widget); void onIncreaseButtonTriggered(); void onDecreaseButtonTriggered(); diff --git a/apps/openmw/mwgui/trainingwindow.cpp b/apps/openmw/mwgui/trainingwindow.cpp index 6463db3d7..5a5f61115 100644 --- a/apps/openmw/mwgui/trainingwindow.cpp +++ b/apps/openmw/mwgui/trainingwindow.cpp @@ -1,16 +1,16 @@ #include "trainingwindow.hpp" -#include - -#include +#include #include "../mwbase/windowmanager.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/dialoguemanager.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwmechanics/npcstats.hpp" @@ -40,12 +40,18 @@ namespace MWGui TrainingWindow::TrainingWindow() : WindowBase("openmw_trainingwindow.layout") , mFadeTimeRemaining(0) + , mTimeAdvancer(0.05) { getWidget(mTrainingOptions, "TrainingOptions"); getWidget(mCancelButton, "CancelButton"); getWidget(mPlayerGold, "PlayerGold"); mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &TrainingWindow::onCancelButtonClicked); + + mTimeAdvancer.eventProgressChanged += MyGUI::newDelegate(this, &TrainingWindow::onTrainingProgressChanged); + mTimeAdvancer.eventFinished += MyGUI::newDelegate(this, &TrainingWindow::onTrainingFinished); + + mProgressBar.setVisible(false); } void TrainingWindow::open() @@ -65,7 +71,7 @@ namespace MWGui MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); int playerGold = player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId); - mPlayerGold->setCaptionWithReplacing("#{sGold}: " + boost::lexical_cast(playerGold)); + mPlayerGold->setCaptionWithReplacing("#{sGold}: " + MyGUI::utility::toString(playerGold)); MWMechanics::NpcStats& npcStats = actor.getClass().getNpcStats (actor); @@ -94,14 +100,13 @@ namespace MWGui int price = MWBase::Environment::get().getMechanicsManager()->getBarterOffer (mPtr,pcStats.getSkill (skills[i].first).getBase() * gmst.find("iTrainingMod")->getInt (),true); - MyGUI::Button* button = mTrainingOptions->createWidget("SandTextButton", + MyGUI::Button* button = mTrainingOptions->createWidget(price <= playerGold ? "SandTextButton" : "SandTextButtonDisabled", // can't use setEnabled since that removes tooltip MyGUI::IntCoord(5, 5+i*18, mTrainingOptions->getWidth()-10, 18), MyGUI::Align::Default); - button->setEnabled(price <= playerGold); button->setUserData(skills[i].first); button->eventMouseButtonClick += MyGUI::newDelegate(this, &TrainingWindow::onTrainingSelected); - button->setCaptionWithReplacing("#{" + ESM::Skill::sSkillNameIds[skills[i].first] + "} - " + boost::lexical_cast(price)); + button->setCaptionWithReplacing("#{" + ESM::Skill::sSkillNameIds[skills[i].first] + "} - " + MyGUI::utility::toString(price)); button->setSize(button->getTextSize ().width+12, button->getSize().height); @@ -134,6 +139,9 @@ namespace MWGui int price = pcStats.getSkill (skillId).getBase() * store.get().find("iTrainingMod")->getInt (); price = MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mPtr,price,true); + if (price > player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId)) + return; + MWMechanics::NpcStats& npcStats = mPtr.getClass().getNpcStats (mPtr); if (npcStats.getSkill (skillId).getBase () <= pcStats.getSkill (skillId).getBase ()) { @@ -159,27 +167,46 @@ namespace MWGui // remove gold player.getClass().getContainerStore(player).remove(MWWorld::ContainerStore::sGoldId, price, player); + // add gold to NPC trading gold pool + npcStats.setGoldPool(npcStats.getGoldPool() + price); + // go back to game mode MWBase::Environment::get().getWindowManager()->removeGuiMode (GM_Training); - MWBase::Environment::get().getWindowManager()->removeGuiMode (GM_Dialogue); + MWBase::Environment::get().getDialogueManager()->goodbyeSelected(); // advance time MWBase::Environment::get().getWorld ()->advanceTime (2); MWBase::Environment::get().getMechanicsManager()->rest(false); MWBase::Environment::get().getMechanicsManager()->rest(false); - MWBase::Environment::get().getWorld ()->getFader()->fadeOut(0.25); + mProgressBar.setVisible(true); + mProgressBar.setProgress(0, 2); + mTimeAdvancer.run(2); + + MWBase::Environment::get().getWindowManager()->fadeScreenOut(0.25); mFadeTimeRemaining = 0.5; } + void TrainingWindow::onTrainingProgressChanged(int cur, int total) + { + mProgressBar.setProgress(cur, total); + } + + void TrainingWindow::onTrainingFinished() + { + mProgressBar.setVisible(false); + } + void TrainingWindow::onFrame(float dt) { + mTimeAdvancer.onFrame(dt); + if (mFadeTimeRemaining <= 0) return; mFadeTimeRemaining -= dt; if (mFadeTimeRemaining <= 0) - MWBase::Environment::get().getWorld ()->getFader()->fadeIn(0.25); + MWBase::Environment::get().getWindowManager()->fadeScreenIn(0.25); } } diff --git a/apps/openmw/mwgui/trainingwindow.hpp b/apps/openmw/mwgui/trainingwindow.hpp index 1fc902b20..7c4582062 100644 --- a/apps/openmw/mwgui/trainingwindow.hpp +++ b/apps/openmw/mwgui/trainingwindow.hpp @@ -3,6 +3,8 @@ #include "windowbase.hpp" #include "referenceinterface.hpp" +#include "timeadvancer.hpp" +#include "waitdialog.hpp" namespace MWGui { @@ -26,11 +28,17 @@ namespace MWGui void onCancelButtonClicked (MyGUI::Widget* sender); void onTrainingSelected(MyGUI::Widget* sender); + void onTrainingProgressChanged(int cur, int total); + void onTrainingFinished(); + MyGUI::Widget* mTrainingOptions; MyGUI::Button* mCancelButton; MyGUI::TextBox* mPlayerGold; float mFadeTimeRemaining; + + WaitDialogProgressBar mProgressBar; + TimeAdvancer mTimeAdvancer; }; } diff --git a/apps/openmw/mwgui/travelwindow.cpp b/apps/openmw/mwgui/travelwindow.cpp index 9aa75173a..50e08223e 100644 --- a/apps/openmw/mwgui/travelwindow.cpp +++ b/apps/openmw/mwgui/travelwindow.cpp @@ -1,16 +1,19 @@ #include "travelwindow.hpp" -#include +#include +#include +#include #include -#include - #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/soundmanager.hpp" +#include "../mwbase/dialoguemanager.hpp" + +#include "../mwmechanics/creaturestats.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" @@ -25,7 +28,6 @@ namespace MWGui TravelWindow::TravelWindow() : WindowBase("openmw_travel_window.layout") , mCurrentY(0) - , mLastPos(0) { setCoord(0, 0, 450, 300); @@ -87,7 +89,7 @@ namespace MWGui oss << price; toAdd->setUserString("price",oss.str()); - toAdd->setCaptionWithReplacing("#{sCell=" + name + "} - " + boost::lexical_cast(price)+"#{sgp}"); + toAdd->setCaptionWithReplacing("#{sCell=" + name + "} - " + MyGUI::utility::toString(price)+"#{sgp}"); toAdd->setSize(toAdd->getTextSize().width,sLineHeight); toAdd->eventMouseWheel += MyGUI::newDelegate(this, &TravelWindow::onMouseWheel); toAdd->setUserString("Destination", name); @@ -126,7 +128,10 @@ namespace MWGui } updateLabels(); + // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden + mDestinationsView->setVisibleVScroll(false); mDestinationsView->setCanvasSize (MyGUI::IntSize(mDestinationsView->getWidth(), std::max(mDestinationsView->getHeight(), mCurrentY))); + mDestinationsView->setVisibleVScroll(true); } void TravelWindow::onTravelButtonClick(MyGUI::Widget* _sender) @@ -147,7 +152,11 @@ namespace MWGui player.getClass().getContainerStore(player).remove(MWWorld::ContainerStore::sGoldId, price, player); - MWBase::Environment::get().getWorld ()->getFader ()->fadeOut(1); + // add gold to NPC trading gold pool + MWMechanics::CreatureStats& npcStats = mPtr.getClass().getCreatureStats(mPtr); + npcStats.setGoldPool(npcStats.getGoldPool() + price); + + MWBase::Environment::get().getWindowManager()->fadeScreenOut(1); ESM::Position pos = *_sender->getUserData(); std::string cellname = _sender->getUserString("Destination"); bool interior = _sender->getUserString("interior") == "y"; @@ -164,14 +173,15 @@ namespace MWGui MWBase::Environment::get().getWorld()->advanceTime(hours); } + MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Travel); + MWBase::Environment::get().getDialogueManager()->goodbyeSelected(); + // Teleports any followers, too. MWWorld::ActionTeleport action(interior ? cellname : "", pos); action.execute(player); - MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Travel); - MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Dialogue); - MWBase::Environment::get().getWorld ()->getFader ()->fadeOut(0); - MWBase::Environment::get().getWorld ()->getFader ()->fadeIn(1); + MWBase::Environment::get().getWindowManager()->fadeScreenOut(0); + MWBase::Environment::get().getWindowManager()->fadeScreenIn(1); } void TravelWindow::onCancelButtonClicked(MyGUI::Widget* _sender) @@ -184,7 +194,7 @@ namespace MWGui MWWorld::Ptr player = MWBase::Environment::get().getWorld ()->getPlayerPtr(); int playerGold = player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId); - mPlayerGold->setCaptionWithReplacing("#{sGold}: " + boost::lexical_cast(playerGold)); + mPlayerGold->setCaptionWithReplacing("#{sGold}: " + MyGUI::utility::toString(playerGold)); mPlayerGold->setCoord(8, mPlayerGold->getTop(), mPlayerGold->getTextSize().width, diff --git a/apps/openmw/mwgui/travelwindow.hpp b/apps/openmw/mwgui/travelwindow.hpp index 4328f7ac2..3230f897f 100644 --- a/apps/openmw/mwgui/travelwindow.hpp +++ b/apps/openmw/mwgui/travelwindow.hpp @@ -1,7 +1,9 @@ #ifndef MWGUI_TravelWINDOW_H #define MWGUI_TravelWINDOW_H -#include "container.hpp" + +#include "windowbase.hpp" +#include "referenceinterface.hpp" namespace MyGUI { @@ -39,7 +41,7 @@ namespace MWGui void onMouseWheel(MyGUI::Widget* _sender, int _rel); void addDestination(const std::string& name, ESM::Position pos, bool interior); void clearDestinations(); - int mLastPos,mCurrentY; + int mCurrentY; static const int sLineHeight; diff --git a/apps/openmw/mwgui/videowidget.cpp b/apps/openmw/mwgui/videowidget.cpp index cfd837a95..046070841 100644 --- a/apps/openmw/mwgui/videowidget.cpp +++ b/apps/openmw/mwgui/videowidget.cpp @@ -1,39 +1,71 @@ #include "videowidget.hpp" +#include + +#include + +#include "../mwsound/movieaudiofactory.hpp" + namespace MWGui { VideoWidget::VideoWidget() { + mPlayer.reset(new Video::VideoPlayer()); setNeedKeyFocus(true); } void VideoWidget::playVideo(const std::string &video) { - mPlayer.playVideo(video); + mPlayer->setAudioFactory(new MWSound::MovieAudioFactory()); + mPlayer->playVideo(video); - setImageTexture(mPlayer.getTextureName()); + setImageTexture(mPlayer->getTextureName()); } int VideoWidget::getVideoWidth() { - return mPlayer.getVideoWidth(); + return mPlayer->getVideoWidth(); } int VideoWidget::getVideoHeight() { - return mPlayer.getVideoHeight(); + return mPlayer->getVideoHeight(); } bool VideoWidget::update() { - mPlayer.update(); - return mPlayer.isPlaying(); + return mPlayer->update(); } void VideoWidget::stop() { - mPlayer.close(); + mPlayer->close(); +} + +bool VideoWidget::hasAudioStream() +{ + return mPlayer->hasAudioStream(); +} + +void VideoWidget::autoResize(bool stretch) +{ + MyGUI::IntSize screenSize = MyGUI::RenderManager::getInstance().getViewSize(); + if (getParent()) + screenSize = getParent()->getSize(); + + if (getVideoHeight() > 0 && !stretch) + { + double imageaspect = static_cast(getVideoWidth())/getVideoHeight(); + + int leftPadding = std::max(0.0, (screenSize.width - screenSize.height * imageaspect) / 2); + int topPadding = std::max(0.0, (screenSize.height - screenSize.width / imageaspect) / 2); + + setCoord(leftPadding, topPadding, + screenSize.width - leftPadding*2, screenSize.height - topPadding*2); + } + else + setCoord(0,0,screenSize.width,screenSize.height); } } diff --git a/apps/openmw/mwgui/videowidget.hpp b/apps/openmw/mwgui/videowidget.hpp index ad3d4d5e3..ee44328eb 100644 --- a/apps/openmw/mwgui/videowidget.hpp +++ b/apps/openmw/mwgui/videowidget.hpp @@ -3,7 +3,10 @@ #include -#include "../mwrender/videoplayer.hpp" +namespace Video +{ + class VideoPlayer; +} namespace MWGui { @@ -26,11 +29,20 @@ namespace MWGui /// @return Is the video still playing? bool update(); + /// Return true if a video is currently playing and it has an audio stream. + bool hasAudioStream(); + /// Stop video and free resources (done automatically on destruction) void stop(); + /// Adjust the coordinates of this video widget relative to its parent, + /// based on the dimensions of the playing video. + /// @param stretch Stretch the video to fill the whole screen? If false, + /// black bars may be added to fix the aspect ratio. + void autoResize (bool stretch); + private: - MWRender::VideoPlayer mPlayer; + std::auto_ptr mPlayer; }; } diff --git a/apps/openmw/mwgui/waitdialog.cpp b/apps/openmw/mwgui/waitdialog.cpp index 9c7757af9..ad873a7c3 100644 --- a/apps/openmw/mwgui/waitdialog.cpp +++ b/apps/openmw/mwgui/waitdialog.cpp @@ -1,8 +1,9 @@ #include "waitdialog.hpp" -#include +#include -#include +#include +#include #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" @@ -12,12 +13,15 @@ #include "../mwworld/class.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwstate/charactermanager.hpp" +#include "widgets.hpp" + namespace MWGui { @@ -37,7 +41,7 @@ namespace MWGui { mProgressBar->setProgressRange (total); mProgressBar->setProgressPosition (cur); - mProgressText->setCaption(boost::lexical_cast(cur) + "/" + boost::lexical_cast(total)); + mProgressText->setCaption(MyGUI::utility::toString(cur) + "/" + MyGUI::utility::toString(total)); } // --------------------------------------------------------------------------------------------------------- @@ -45,12 +49,11 @@ namespace MWGui WaitDialog::WaitDialog() : WindowBase("openmw_wait_dialog.layout") , mProgressBar() - , mWaiting(false) + , mTimeAdvancer(0.05) , mSleeping(false) , mHours(1) - , mRemainingTime(0.05) - , mCurHour(0) , mManualHours(1) + , mFadeTimeRemaining(0) , mInterruptAt(-1) { getWidget(mDateTimeText, "DateTimeText"); @@ -66,6 +69,10 @@ namespace MWGui mWaitButton->eventMouseButtonClick += MyGUI::newDelegate(this, &WaitDialog::onWaitButtonClicked); mHourSlider->eventScrollChangePosition += MyGUI::newDelegate(this, &WaitDialog::onHourSliderChangedPosition); + mTimeAdvancer.eventProgressChanged += MyGUI::newDelegate(this, &WaitDialog::onWaitingProgressChanged); + mTimeAdvancer.eventInterrupted += MyGUI::newDelegate(this, &WaitDialog::onWaitingInterrupted); + mTimeAdvancer.eventFinished += MyGUI::newDelegate(this, &WaitDialog::onWaitingFinished); + mProgressBar.setVisible (false); } @@ -103,9 +110,9 @@ namespace MWGui if (hour == 0) hour = 12; std::string dateTimeText = - boost::lexical_cast(MWBase::Environment::get().getWorld ()->getDay ()) + " " - + month + " (#{sDay} " + boost::lexical_cast(MWBase::Environment::get().getWorld ()->getTimeStamp ().getDay()) - + ") " + boost::lexical_cast(hour) + " " + (pm ? "#{sSaveMenuHelp05}" : "#{sSaveMenuHelp04}"); + MyGUI::utility::toString(MWBase::Environment::get().getWorld ()->getDay ()) + " " + + month + " (#{sDay} " + MyGUI::utility::toString(MWBase::Environment::get().getWorld ()->getTimeStamp ().getDay()) + + ") " + MyGUI::utility::toString(hour) + " " + (pm ? "#{sSaveMenuHelp05}" : "#{sSaveMenuHelp04}"); mDateTimeText->setCaptionWithReplacing (dateTimeText); } @@ -128,12 +135,10 @@ namespace MWGui MWBase::Environment::get().getStateManager()->quickSave("Autosave"); MWBase::World* world = MWBase::Environment::get().getWorld(); - world->getFader ()->fadeOut(0.2); + MWBase::Environment::get().getWindowManager()->fadeScreenOut(0.2); + mFadeTimeRemaining = 0.4; setVisible(false); - mProgressBar.setVisible (true); - mWaiting = true; - mCurHour = 0; mHours = hoursToWait; // FIXME: move this somewhere else? @@ -160,8 +165,7 @@ namespace MWGui } } - mRemainingTime = 0.05; - mProgressBar.setProgress (0, mHours); + mProgressBar.setProgress (0, hoursToWait); } void WaitDialog::onCancelButtonClicked(MyGUI::Widget* sender) @@ -171,16 +175,45 @@ namespace MWGui void WaitDialog::onHourSliderChangedPosition(MyGUI::ScrollBar* sender, size_t position) { - mHourText->setCaptionWithReplacing (boost::lexical_cast(position+1) + " #{sRestMenu2}"); + mHourText->setCaptionWithReplacing (MyGUI::utility::toString(position+1) + " #{sRestMenu2}"); mManualHours = position+1; } + void WaitDialog::onWaitingProgressChanged(int cur, int total) + { + mProgressBar.setProgress(cur, total); + MWBase::Environment::get().getWorld()->advanceTime(1); + MWBase::Environment::get().getMechanicsManager()->rest(mSleeping); + } + + void WaitDialog::onWaitingInterrupted() + { + MWBase::Environment::get().getWindowManager()->messageBox("#{sSleepInterrupt}"); + MWBase::Environment::get().getWorld()->spawnRandomCreature(mInterruptCreatureList); + stopWaiting(); + } + + void WaitDialog::onWaitingFinished() + { + stopWaiting(); + + MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); + const MWMechanics::NpcStats &pcstats = player.getClass().getNpcStats(player); + + // trigger levelup if possible + const MWWorld::Store &gmst = + MWBase::Environment::get().getWorld()->getStore().get(); + if (mSleeping && pcstats.getLevelProgress () >= gmst.find("iLevelUpTotal")->getInt()) + { + MWBase::Environment::get().getWindowManager()->pushGuiMode (GM_Levelup); + } + } + void WaitDialog::setCanRest (bool canRest) { MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); - bool full = (stats.getFatigue().getCurrent() >= stats.getFatigue().getModified()) - && (stats.getHealth().getCurrent() >= stats.getHealth().getModified()) + bool full = (stats.getHealth().getCurrent() >= stats.getHealth().getModified()) && (stats.getMagicka().getCurrent() >= stats.getMagicka().getModified()); MWMechanics::NpcStats& npcstats = player.getClass().getNpcStats(player); bool werewolf = npcstats.isWerewolf(); @@ -193,68 +226,43 @@ namespace MWGui mSleeping = canRest; - dynamic_cast(mMainWidget)->notifyChildrenSizeChanged(); + Gui::Box* box = dynamic_cast(mMainWidget); + if (box == NULL) + throw std::runtime_error("main widget must be a box"); + box->notifyChildrenSizeChanged(); center(); } void WaitDialog::onFrame(float dt) { - if (!mWaiting) - return; - - if (mCurHour == mInterruptAt) - { - MWBase::Environment::get().getWindowManager()->messageBox("#{sSleepInterrupt}"); - MWBase::Environment::get().getWorld()->spawnRandomCreature(mInterruptCreatureList); - stopWaiting(); - } + mTimeAdvancer.onFrame(dt); - mRemainingTime -= dt; - - while (mRemainingTime < 0) - { - mRemainingTime += 0.05; - ++mCurHour; - mProgressBar.setProgress (mCurHour, mHours); + if (mFadeTimeRemaining <= 0) + return; - if (mCurHour <= mHours) - { - MWBase::Environment::get().getWorld ()->advanceTime (1); - MWBase::Environment::get().getMechanicsManager ()->rest (mSleeping); - } - } + mFadeTimeRemaining -= dt; - if (mCurHour > mHours) + if (mFadeTimeRemaining <= 0) { - stopWaiting(); - - MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); - const MWMechanics::NpcStats &pcstats = player.getClass().getNpcStats(player); - - // trigger levelup if possible - const MWWorld::Store &gmst = - MWBase::Environment::get().getWorld()->getStore().get(); - if (mSleeping && pcstats.getLevelProgress () >= gmst.find("iLevelUpTotal")->getInt()) - { - MWBase::Environment::get().getWindowManager()->pushGuiMode (GM_Levelup); - } + mProgressBar.setVisible(true); + mTimeAdvancer.run(mHours, mInterruptAt); } } void WaitDialog::stopWaiting () { - MWBase::Environment::get().getWorld ()->getFader ()->fadeIn(0.2); + MWBase::Environment::get().getWindowManager()->fadeScreenIn(0.2); mProgressBar.setVisible (false); MWBase::Environment::get().getWindowManager()->removeGuiMode (GM_Rest); MWBase::Environment::get().getWindowManager()->removeGuiMode (GM_RestBed); - mWaiting = false; + mTimeAdvancer.stop(); } void WaitDialog::wakeUp () { mSleeping = false; - mWaiting = false; + mTimeAdvancer.stop(); stopWaiting(); } diff --git a/apps/openmw/mwgui/waitdialog.hpp b/apps/openmw/mwgui/waitdialog.hpp index 1cf05bb06..e8f58c574 100644 --- a/apps/openmw/mwgui/waitdialog.hpp +++ b/apps/openmw/mwgui/waitdialog.hpp @@ -1,12 +1,18 @@ #ifndef MWGUI_WAIT_DIALOG_H #define MWGUI_WAIT_DIALOG_H +#include "timeadvancer.hpp" + #include "windowbase.hpp" -#include "widgets.hpp" namespace MWGui { + namespace Widgets + { + class MWScrollBar; + } + class WaitDialogProgressBar : public WindowBase { public: @@ -34,7 +40,7 @@ namespace MWGui void bedActivated() { setCanRest(true); } - bool getSleeping() { return mWaiting && mSleeping; } + bool getSleeping() { return mTimeAdvancer.isRunning() && mSleeping; } void wakeUp(); void autosave(); @@ -47,12 +53,11 @@ namespace MWGui MyGUI::Button* mCancelButton; MWGui::Widgets::MWScrollBar* mHourSlider; - bool mWaiting; + TimeAdvancer mTimeAdvancer; bool mSleeping; - int mCurHour; int mHours; int mManualHours; // stores the hours to rest selected via slider - float mRemainingTime; + float mFadeTimeRemaining; int mInterruptAt; std::string mInterruptCreatureList; @@ -64,6 +69,10 @@ namespace MWGui void onCancelButtonClicked(MyGUI::Widget* sender); void onHourSliderChangedPosition(MyGUI::ScrollBar* sender, size_t position); + void onWaitingProgressChanged(int cur, int total); + void onWaitingInterrupted(); + void onWaitingFinished(); + void setCanRest(bool canRest); void startWaiting(int hoursToWait); diff --git a/apps/openmw/mwgui/widgets.cpp b/apps/openmw/mwgui/widgets.cpp index de48a9d3e..26fe31567 100644 --- a/apps/openmw/mwgui/widgets.cpp +++ b/apps/openmw/mwgui/widgets.cpp @@ -1,7 +1,5 @@ #include "widgets.hpp" -#include - #include #include @@ -9,10 +7,16 @@ #include #include +#include + #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwworld/esmstore.hpp" + +#include "controllers.hpp" + #undef min #undef max @@ -20,21 +24,6 @@ namespace MWGui { namespace Widgets { - - /* Helper functions */ - - /* - * Fixes the filename of a texture path to use the correct .dds extension. - * This is needed on some ESM entries which point to a .tga file instead. - */ - void fixTexturePath(std::string &path) - { - int offset = path.rfind("."); - if (offset < 0) - return; - path.replace(offset, path.length() - offset, ".dds"); - } - /* MWSkill */ MWSkill::MWSkill() @@ -72,18 +61,18 @@ namespace MWGui { if (mSkillId == ESM::Skill::Length) { - static_cast(mSkillNameWidget)->setCaption(""); + mSkillNameWidget->setCaption(""); } else { const std::string &name = MWBase::Environment::get().getWindowManager()->getGameSettingString(ESM::Skill::sSkillNameIds[mSkillId], ""); - static_cast(mSkillNameWidget)->setCaption(name); + mSkillNameWidget->setCaption(name); } } if (mSkillValueWidget) { SkillValue::Type modified = mValue.getModified(), base = mValue.getBase(); - static_cast(mSkillValueWidget)->setCaption(boost::lexical_cast(modified)); + mSkillValueWidget->setCaption(MyGUI::utility::toString(modified)); if (modified > base) mSkillValueWidget->_setWidgetState("increased"); else if (modified < base) @@ -158,7 +147,7 @@ namespace MWGui { if (mId < 0 || mId >= 8) { - static_cast(mAttributeNameWidget)->setCaption(""); + mAttributeNameWidget->setCaption(""); } else { @@ -173,13 +162,13 @@ namespace MWGui "sAttributeLuck" }; const std::string &name = MWBase::Environment::get().getWindowManager()->getGameSettingString(attributes[mId], ""); - static_cast(mAttributeNameWidget)->setCaption(name); + mAttributeNameWidget->setCaption(name); } } if (mAttributeValueWidget) { int modified = mValue.getModified(), base = mValue.getBase(); - static_cast(mAttributeValueWidget)->setCaption(boost::lexical_cast(modified)); + mAttributeValueWidget->setCaption(MyGUI::utility::toString(modified)); if (modified > base) mAttributeValueWidget->_setWidgetState("increased"); else if (modified < base) @@ -238,11 +227,10 @@ namespace MWGui const ESM::Spell *spell = store.get().search(mId); MYGUI_ASSERT(spell, "spell with id '" << mId << "' not found"); - MWSpellEffectPtr effect = NULL; std::vector::const_iterator end = spell->mEffects.mList.end(); for (std::vector::const_iterator it = spell->mEffects.mList.begin(); it != end; ++it) { - effect = creator->createWidget("MW_EffectImage", coord, MyGUI::Align::Default); + MWSpellEffectPtr effect = creator->createWidget("MW_EffectImage", coord, MyGUI::Align::Default); SpellEffectParams params; params.mEffectID = it->mEffectID; params.mSkill = it->mSkill; @@ -269,9 +257,9 @@ namespace MWGui const ESM::Spell *spell = store.get().search(mId); if (spell) - static_cast(mSpellNameWidget)->setCaption(spell->mName); + mSpellNameWidget->setCaption(spell->mName); else - static_cast(mSpellNameWidget)->setCaption(""); + mSpellNameWidget->setCaption(""); } } @@ -323,7 +311,7 @@ namespace MWGui // ... then adjust the size for all widgets for (std::vector::iterator it = effects.begin(); it != effects.end(); ++it) { - effect = static_cast(*it); + effect = (*it)->castType(); bool needcenter = center && (maxwidth > effect->getRequestedWidth()); int diff = maxwidth - effect->getRequestedWidth(); if (needcenter) @@ -442,9 +430,9 @@ namespace MWGui spellLine += formatter.str(); } else if ( displayType != ESM::MagicEffect::MDT_None ) { - spellLine += " " + boost::lexical_cast(mEffectParams.mMagnMin); + spellLine += " " + MyGUI::utility::toString(mEffectParams.mMagnMin); if (mEffectParams.mMagnMin != mEffectParams.mMagnMax) - spellLine += to + boost::lexical_cast(mEffectParams.mMagnMax); + spellLine += to + MyGUI::utility::toString(mEffectParams.mMagnMax); if ( displayType == ESM::MagicEffect::MDT_Percentage ) spellLine += pct; @@ -460,14 +448,14 @@ namespace MWGui // constant effects have no duration and no target if (!mEffectParams.mIsConstant) { - if (mEffectParams.mDuration >= 0 && !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration)) + if (mEffectParams.mDuration > 0 && !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration)) { - spellLine += " " + MWBase::Environment::get().getWindowManager()->getGameSettingString("sfor", "") + " " + boost::lexical_cast(mEffectParams.mDuration) + ((mEffectParams.mDuration == 1) ? sec : secs); + spellLine += " " + MWBase::Environment::get().getWindowManager()->getGameSettingString("sfor", "") + " " + MyGUI::utility::toString(mEffectParams.mDuration) + ((mEffectParams.mDuration == 1) ? sec : secs); } if (mEffectParams.mArea > 0) { - spellLine += " #{sin} " + boost::lexical_cast(mEffectParams.mArea) + " #{sfootarea}"; + spellLine += " #{sin} " + MyGUI::utility::toString(mEffectParams.mArea) + " #{sfootarea}"; } // potions have no target @@ -483,12 +471,10 @@ namespace MWGui } } - static_cast(mTextWidget)->setCaptionWithReplacing(spellLine); + mTextWidget->setCaptionWithReplacing(spellLine); mRequestedWidth = mTextWidget->getTextSize().width + 24; - std::string path = std::string("icons\\") + magicEffect->mIcon; - fixTexturePath(path); - mImageWidget->setImageTexture(path); + mImageWidget->setImageTexture(Misc::ResourceHelpers::correctIconPath(magicEffect->mIcon)); } MWSpellEffect::~MWSpellEffect() @@ -530,13 +516,13 @@ namespace MWGui { std::stringstream out; out << mValue << "/" << mMax; - static_cast(mBarTextWidget)->setCaption(out.str().c_str()); + mBarTextWidget->setCaption(out.str().c_str()); } } void MWDynamicStat::setTitle(const std::string& text) { if (mTextWidget) - static_cast(mTextWidget)->setCaption(text); + mTextWidget->setCaption(text); } MWDynamicStat::~MWDynamicStat() @@ -552,401 +538,6 @@ namespace MWGui assignWidget(mBarTextWidget, "BarText"); } - - - - // --------------------------------------------------------------------------------------------------------------------- - - void AutoSizedWidget::notifySizeChange (MyGUI::Widget* w) - { - if (w->getParent () != 0) - { - Box* b = dynamic_cast(w->getParent()); - if (b) - b->notifyChildrenSizeChanged (); - else - { - if (mExpandDirection == MyGUI::Align::Left) - { - int hdiff = getRequestedSize ().width - w->getSize().width; - w->setPosition(w->getPosition() - MyGUI::IntPoint(hdiff, 0)); - } - w->setSize(getRequestedSize ()); - } - } - } - - - MyGUI::IntSize AutoSizedTextBox::getRequestedSize() - { - return getTextSize(); - } - - void AutoSizedTextBox::setCaption(const MyGUI::UString& _value) - { - TextBox::setCaption(_value); - - notifySizeChange (this); - } - - void AutoSizedTextBox::setPropertyOverride(const std::string& _key, const std::string& _value) - { - if (_key == "ExpandDirection") - { - mExpandDirection = MyGUI::Align::parse (_value); - } - else - { - TextBox::setPropertyOverride (_key, _value); - } - } - - MyGUI::IntSize AutoSizedEditBox::getRequestedSize() - { - if (getAlign().isHStretch()) - throw std::runtime_error("AutoSizedEditBox can't have HStretch align (" + getName() + ")"); - return MyGUI::IntSize(getSize().width, getTextSize().height); - } - - void AutoSizedEditBox::setCaption(const MyGUI::UString& _value) - { - EditBox::setCaption(_value); - - notifySizeChange (this); - } - - void AutoSizedEditBox::setPropertyOverride(const std::string& _key, const std::string& _value) - { - if (_key == "ExpandDirection") - { - mExpandDirection = MyGUI::Align::parse (_value); - } - else - { - EditBox::setPropertyOverride (_key, _value); - } - } - - - MyGUI::IntSize AutoSizedButton::getRequestedSize() - { - MyGUI::IntSize padding(24, 8); - if (isUserString("TextPadding")) - padding = MyGUI::IntSize::parse(getUserString("TextPadding")); - - MyGUI::IntSize size = getTextSize() + MyGUI::IntSize(padding.width,padding.height); - return size; - } - - void AutoSizedButton::setCaption(const MyGUI::UString& _value) - { - Button::setCaption(_value); - - notifySizeChange (this); - } - - void AutoSizedButton::setPropertyOverride(const std::string& _key, const std::string& _value) - { - if (_key == "ExpandDirection") - { - mExpandDirection = MyGUI::Align::parse (_value); - } - else - { - Button::setPropertyOverride (_key, _value); - } - } - - Box::Box() - : mSpacing(4) - , mPadding(0) - , mAutoResize(false) - { - - } - - void Box::notifyChildrenSizeChanged () - { - align(); - } - - void Box::_setPropertyImpl(const std::string& _key, const std::string& _value) - { - if (_key == "Spacing") - mSpacing = MyGUI::utility::parseValue(_value); - else if (_key == "Padding") - mPadding = MyGUI::utility::parseValue(_value); - else if (_key == "AutoResize") - mAutoResize = MyGUI::utility::parseValue(_value); - } - - void HBox::align () - { - unsigned int count = getChildCount (); - size_t h_stretched_count = 0; - int total_width = 0; - int total_height = 0; - std::vector< std::pair > sizes; - sizes.resize(count); - - for (unsigned int i = 0; i < count; ++i) - { - MyGUI::Widget* w = getChildAt(i); - bool hstretch = w->getUserString ("HStretch") == "true"; - bool hidden = w->getUserString("Hidden") == "true"; - if (hidden) - continue; - h_stretched_count += hstretch; - AutoSizedWidget* aw = dynamic_cast(w); - if (aw) - { - sizes[i] = std::make_pair(aw->getRequestedSize (), hstretch); - total_width += aw->getRequestedSize ().width; - total_height = std::max(total_height, aw->getRequestedSize ().height); - } - else - { - sizes[i] = std::make_pair(w->getSize(), hstretch); - total_width += w->getSize().width; - if (!(w->getUserString("VStretch") == "true")) - total_height = std::max(total_height, w->getSize().height); - } - - if (i != count-1) - total_width += mSpacing; - } - - if (mAutoResize && (total_width+mPadding*2 != getSize().width || total_height+mPadding*2 != getSize().height)) - { - setSize(MyGUI::IntSize(total_width+mPadding*2, total_height+mPadding*2)); - return; - } - - - int curX = 0; - for (unsigned int i = 0; i < count; ++i) - { - if (i == 0) - curX += mPadding; - - MyGUI::Widget* w = getChildAt(i); - - bool hidden = w->getUserString("Hidden") == "true"; - if (hidden) - continue; - - bool vstretch = w->getUserString ("VStretch") == "true"; - int max_height = getSize().height - mPadding*2; - int height = vstretch ? max_height : sizes[i].first.height; - - MyGUI::IntCoord widgetCoord; - widgetCoord.left = curX; - widgetCoord.top = mPadding + (getSize().height-mPadding*2 - height) / 2; - int width = sizes[i].second ? sizes[i].first.width + (getSize().width-mPadding*2 - total_width)/h_stretched_count - : sizes[i].first.width; - widgetCoord.width = width; - widgetCoord.height = height; - w->setCoord(widgetCoord); - curX += width; - - if (i != count-1) - curX += mSpacing; - } - } - - void HBox::setPropertyOverride(const std::string& _key, const std::string& _value) - { - Box::_setPropertyImpl (_key, _value); - } - - void HBox::setSize (const MyGUI::IntSize& _value) - { - MyGUI::Widget::setSize (_value); - align(); - } - - void HBox::setCoord (const MyGUI::IntCoord& _value) - { - MyGUI::Widget::setCoord (_value); - align(); - } - - void HBox::onWidgetCreated(MyGUI::Widget* _widget) - { - align(); - } - - MyGUI::IntSize HBox::getRequestedSize () - { - MyGUI::IntSize size(0,0); - for (unsigned int i = 0; i < getChildCount (); ++i) - { - bool hidden = getChildAt(i)->getUserString("Hidden") == "true"; - if (hidden) - continue; - - AutoSizedWidget* w = dynamic_cast(getChildAt(i)); - if (w) - { - MyGUI::IntSize requested = w->getRequestedSize (); - size.height = std::max(size.height, requested.height); - size.width = size.width + requested.width; - if (i != getChildCount()-1) - size.width += mSpacing; - } - else - { - MyGUI::IntSize requested = getChildAt(i)->getSize (); - size.height = std::max(size.height, requested.height); - - if (getChildAt(i)->getUserString("HStretch") != "true") - size.width = size.width + requested.width; - - if (i != getChildCount()-1) - size.width += mSpacing; - } - size.height += mPadding*2; - size.width += mPadding*2; - } - return size; - } - - - - - void VBox::align () - { - unsigned int count = getChildCount (); - size_t v_stretched_count = 0; - int total_height = 0; - int total_width = 0; - std::vector< std::pair > sizes; - sizes.resize(count); - for (unsigned int i = 0; i < count; ++i) - { - MyGUI::Widget* w = getChildAt(i); - - bool hidden = w->getUserString("Hidden") == "true"; - if (hidden) - continue; - - bool vstretch = w->getUserString ("VStretch") == "true"; - v_stretched_count += vstretch; - AutoSizedWidget* aw = dynamic_cast(w); - if (aw) - { - sizes[i] = std::make_pair(aw->getRequestedSize (), vstretch); - total_height += aw->getRequestedSize ().height; - total_width = std::max(total_width, aw->getRequestedSize ().width); - } - else - { - sizes[i] = std::make_pair(w->getSize(), vstretch); - total_height += w->getSize().height; - - if (!(w->getUserString("HStretch") == "true")) - total_width = std::max(total_width, w->getSize().width); - } - - if (i != count-1) - total_height += mSpacing; - } - - if (mAutoResize && (total_width+mPadding*2 != getSize().width || total_height+mPadding*2 != getSize().height)) - { - setSize(MyGUI::IntSize(total_width+mPadding*2, total_height+mPadding*2)); - return; - } - - - int curY = 0; - for (unsigned int i = 0; i < count; ++i) - { - if (i==0) - curY += mPadding; - - MyGUI::Widget* w = getChildAt(i); - - bool hidden = w->getUserString("Hidden") == "true"; - if (hidden) - continue; - - bool hstretch = w->getUserString ("HStretch") == "true"; - int maxWidth = getSize().width - mPadding*2; - int width = hstretch ? maxWidth : sizes[i].first.width; - - MyGUI::IntCoord widgetCoord; - widgetCoord.top = curY; - widgetCoord.left = mPadding + (getSize().width-mPadding*2 - width) / 2; - int height = sizes[i].second ? sizes[i].first.height + (getSize().height-mPadding*2 - total_height)/v_stretched_count - : sizes[i].first.height; - widgetCoord.height = height; - widgetCoord.width = width; - w->setCoord(widgetCoord); - curY += height; - - if (i != count-1) - curY += mSpacing; - } - } - - void VBox::setPropertyOverride(const std::string& _key, const std::string& _value) - { - Box::_setPropertyImpl (_key, _value); - } - - void VBox::setSize (const MyGUI::IntSize& _value) - { - MyGUI::Widget::setSize (_value); - align(); - } - - void VBox::setCoord (const MyGUI::IntCoord& _value) - { - MyGUI::Widget::setCoord (_value); - align(); - } - - MyGUI::IntSize VBox::getRequestedSize () - { - MyGUI::IntSize size(0,0); - for (unsigned int i = 0; i < getChildCount (); ++i) - { - bool hidden = getChildAt(i)->getUserString("Hidden") == "true"; - if (hidden) - continue; - - AutoSizedWidget* w = dynamic_cast(getChildAt(i)); - if (w) - { - MyGUI::IntSize requested = w->getRequestedSize (); - size.width = std::max(size.width, requested.width); - size.height = size.height + requested.height; - if (i != getChildCount()-1) - size.height += mSpacing; - } - else - { - MyGUI::IntSize requested = getChildAt(i)->getSize (); - size.width = std::max(size.width, requested.width); - - if (getChildAt(i)->getUserString("VStretch") != "true") - size.height = size.height + requested.height; - - if (i != getChildCount()-1) - size.height += mSpacing; - } - size.height += mPadding*2; - size.width += mPadding*2; - } - return size; - } - - void VBox::onWidgetCreated(MyGUI::Widget* _widget) - { - align(); - } - MWScrollBar::MWScrollBar() : mEnableRepeat(true) , mRepeatTriggerTime(0.5) @@ -975,22 +566,6 @@ namespace MWGui } } - void MWScrollBar::setEnableRepeat(bool enable) - { - mEnableRepeat = enable; - } - - bool MWScrollBar::getEnableRepeat() - { - return mEnableRepeat; - } - - void MWScrollBar::getRepeat(float &trigger, float &step) - { - trigger = mRepeatTriggerTime; - step = mRepeatStepTime; - } - void MWScrollBar::setRepeat(float trigger, float step) { mRepeatTriggerTime = trigger; @@ -1027,8 +602,8 @@ namespace MWGui void MWScrollBar::onDecreaseButtonPressed(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id) { mIsIncreasing = false; - MyGUI::ControllerItem* item = MyGUI::ControllerManager::getInstance().createItem(MWGui::Controllers::ControllerRepeatClick::getClassTypeName()); - MWGui::Controllers::ControllerRepeatClick* controller = item->castType(); + MyGUI::ControllerItem* item = MyGUI::ControllerManager::getInstance().createItem(MWGui::Controllers::ControllerRepeatEvent::getClassTypeName()); + MWGui::Controllers::ControllerRepeatEvent* controller = item->castType(); controller->eventRepeatClick += newDelegate(this, &MWScrollBar::repeatClick); controller->setEnabled(mEnableRepeat); controller->setRepeat(mRepeatTriggerTime, mRepeatStepTime); @@ -1043,8 +618,8 @@ namespace MWGui void MWScrollBar::onIncreaseButtonPressed(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id) { mIsIncreasing = true; - MyGUI::ControllerItem* item = MyGUI::ControllerManager::getInstance().createItem(MWGui::Controllers::ControllerRepeatClick::getClassTypeName()); - MWGui::Controllers::ControllerRepeatClick* controller = item->castType(); + MyGUI::ControllerItem* item = MyGUI::ControllerManager::getInstance().createItem(MWGui::Controllers::ControllerRepeatEvent::getClassTypeName()); + MWGui::Controllers::ControllerRepeatEvent* controller = item->castType(); controller->eventRepeatClick += newDelegate(this, &MWScrollBar::repeatClick); controller->setEnabled(mEnableRepeat); controller->setRepeat(mRepeatTriggerTime, mRepeatStepTime); diff --git a/apps/openmw/mwgui/widgets.hpp b/apps/openmw/mwgui/widgets.hpp index adc56f423..09a3c11ac 100644 --- a/apps/openmw/mwgui/widgets.hpp +++ b/apps/openmw/mwgui/widgets.hpp @@ -1,9 +1,10 @@ #ifndef MWGUI_WIDGETS_H #define MWGUI_WIDGETS_H -#include "../mwworld/esmstore.hpp" #include "../mwmechanics/stat.hpp" -#include "controllers.hpp" + +#include +#include #include #include @@ -12,6 +13,7 @@ namespace MyGUI { class ImageBox; + class ControllerItem; } namespace MWBase @@ -122,8 +124,8 @@ namespace MWGui ESM::Skill::SkillEnum mSkillId; SkillValue mValue; - MyGUI::Widget* mSkillNameWidget; - MyGUI::Widget* mSkillValueWidget; + MyGUI::TextBox* mSkillNameWidget; + MyGUI::TextBox* mSkillValueWidget; }; typedef MWSkill* MWSkillPtr; @@ -162,8 +164,8 @@ namespace MWGui int mId; AttributeValue mValue; - MyGUI::Widget* mAttributeNameWidget; - MyGUI::Widget* mAttributeValueWidget; + MyGUI::TextBox* mAttributeNameWidget; + MyGUI::TextBox* mAttributeValueWidget; }; typedef MWAttribute* MWAttributePtr; @@ -293,123 +295,12 @@ namespace MWGui int mValue, mMax; MyGUI::TextBox* mTextWidget; - MyGUI::ProgressPtr mBarWidget; + MyGUI::ProgressBar* mBarWidget; MyGUI::TextBox* mBarTextWidget; }; typedef MWDynamicStat* MWDynamicStatPtr; - - - - - // --------------------------------------------------------------------------------------------------------------------- - - - - class AutoSizedWidget - { - public: - virtual MyGUI::IntSize getRequestedSize() = 0; - - protected: - void notifySizeChange(MyGUI::Widget* w); - - MyGUI::Align mExpandDirection; - }; - - class AutoSizedTextBox : public AutoSizedWidget, public MyGUI::TextBox - { - MYGUI_RTTI_DERIVED( AutoSizedTextBox ) - - public: - virtual MyGUI::IntSize getRequestedSize(); - virtual void setCaption(const MyGUI::UString& _value); - - protected: - virtual void setPropertyOverride(const std::string& _key, const std::string& _value); - }; - - class AutoSizedEditBox : public AutoSizedWidget, public MyGUI::EditBox - { - MYGUI_RTTI_DERIVED( AutoSizedEditBox ) - - public: - virtual MyGUI::IntSize getRequestedSize(); - virtual void setCaption(const MyGUI::UString& _value); - - protected: - virtual void setPropertyOverride(const std::string& _key, const std::string& _value); - }; - - class AutoSizedButton : public AutoSizedWidget, public MyGUI::Button - { - MYGUI_RTTI_DERIVED( AutoSizedButton ) - - public: - virtual MyGUI::IntSize getRequestedSize(); - virtual void setCaption(const MyGUI::UString& _value); - - protected: - virtual void setPropertyOverride(const std::string& _key, const std::string& _value); - }; - - /** - * @brief A container widget that automatically sizes its children - * @note the box being an AutoSizedWidget as well allows to put boxes inside a box - */ - class Box : public AutoSizedWidget - { - public: - Box(); - - void notifyChildrenSizeChanged(); - - protected: - virtual void align() = 0; - - virtual void _setPropertyImpl(const std::string& _key, const std::string& _value); - - int mSpacing; // how much space to put between elements - - int mPadding; // outer padding - - bool mAutoResize; // auto resize the box so that it exactly fits all elements - }; - - class HBox : public Box, public MyGUI::Widget - { - MYGUI_RTTI_DERIVED( HBox ) - - public: - virtual void setSize (const MyGUI::IntSize &_value); - virtual void setCoord (const MyGUI::IntCoord &_value); - - protected: - virtual void align(); - virtual MyGUI::IntSize getRequestedSize(); - - virtual void setPropertyOverride(const std::string& _key, const std::string& _value); - - virtual void onWidgetCreated(MyGUI::Widget* _widget); - }; - - class VBox : public Box, public MyGUI::Widget - { - MYGUI_RTTI_DERIVED( VBox) - - public: - virtual void setSize (const MyGUI::IntSize &_value); - virtual void setCoord (const MyGUI::IntCoord &_value); - - protected: - virtual void align(); - virtual MyGUI::IntSize getRequestedSize(); - - virtual void setPropertyOverride(const std::string& _key, const std::string& _value); - - virtual void onWidgetCreated(MyGUI::Widget* _widget); - }; - + // Should be removed when upgrading to MyGUI 3.2.2 (current git), it has ScrollBar autorepeat support class MWScrollBar : public MyGUI::ScrollBar { MYGUI_RTTI_DERIVED(MWScrollBar) @@ -418,9 +309,6 @@ namespace MWGui MWScrollBar(); virtual ~MWScrollBar(); - void setEnableRepeat(bool enable); - bool getEnableRepeat(); - void getRepeat(float &trigger, float &step); void setRepeat(float trigger, float step); protected: diff --git a/apps/openmw/mwgui/windowbase.cpp b/apps/openmw/mwgui/windowbase.cpp index 4af0afc1f..8fdcf6b20 100644 --- a/apps/openmw/mwgui/windowbase.cpp +++ b/apps/openmw/mwgui/windowbase.cpp @@ -1,9 +1,13 @@ #include "windowbase.hpp" +#include + +#include + #include "../mwbase/windowmanager.hpp" -#include "container.hpp" #include "../mwbase/environment.hpp" -#include "../mwgui/windowmanagerimp.hpp" + +#include "draganddrop.hpp" using namespace MWGui; @@ -23,6 +27,7 @@ void WindowBase::setVisible(bool visible) close(); // This is needed as invisible widgets can retain key focus. + // Remove for MyGUI 3.2.2 if (!visible) { MyGUI::Widget* keyFocus = MyGUI::InputManager::getInstance().getKeyFocusWidget(); @@ -75,6 +80,8 @@ void WindowModal::close() NoDrop::NoDrop(DragAndDrop *drag, MyGUI::Widget *widget) : mDrag(drag), mWidget(widget), mTransparent(false) { + if (!mWidget) + throw std::runtime_error("NoDrop needs a non-NULL widget!"); } void NoDrop::onFrame(float dt) @@ -96,11 +103,16 @@ void NoDrop::onFrame(float dt) if (mTransparent) { mWidget->setNeedMouseFocus(false); // Allow click-through - mWidget->setAlpha(std::max(0.13f, mWidget->getAlpha() - dt*5)); + setAlpha(std::max(0.13f, mWidget->getAlpha() - dt*5)); } else { mWidget->setNeedMouseFocus(true); - mWidget->setAlpha(std::min(1.0f, mWidget->getAlpha() + dt*5)); + setAlpha(std::min(1.0f, mWidget->getAlpha() + dt*5)); } } + +void NoDrop::setAlpha(float alpha) +{ + mWidget->setAlpha(alpha); +} diff --git a/apps/openmw/mwgui/windowbase.hpp b/apps/openmw/mwgui/windowbase.hpp index 81073d419..bf74c8bf0 100644 --- a/apps/openmw/mwgui/windowbase.hpp +++ b/apps/openmw/mwgui/windowbase.hpp @@ -32,11 +32,6 @@ namespace MWGui ///Returns the visibility state of the window virtual bool isVisible(); void center(); - - /** Event : Dialog finished, OK button clicked.\n - signature : void method()\n - */ - EventHandle_WindowBase eventDone; }; @@ -60,6 +55,7 @@ namespace MWGui NoDrop(DragAndDrop* drag, MyGUI::Widget* widget); void onFrame(float dt); + virtual void setAlpha(float alpha); private: MyGUI::Widget* mWidget; diff --git a/apps/openmw/mwgui/windowmanagerimp.cpp b/apps/openmw/mwgui/windowmanagerimp.cpp index 9ebafc90c..c74cff31c 100644 --- a/apps/openmw/mwgui/windowmanagerimp.cpp +++ b/apps/openmw/mwgui/windowmanagerimp.cpp @@ -5,23 +5,45 @@ #include #include - -#include "MyGUI_UString.h" -#include "MyGUI_IPointer.h" -#include "MyGUI_ResourceImageSetPointer.h" -#include "MyGUI_TextureUtility.h" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include #include #include #include +#include + +#include + +#include +#include + #include "../mwbase/inputmanager.hpp" #include "../mwbase/statemanager.hpp" #include "../mwworld/class.hpp" #include "../mwworld/player.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwworld/esmstore.hpp" + +#include "../mwmechanics/stat.hpp" +#include "../mwmechanics/npcstats.hpp" #include "../mwsound/soundmanagerimp.hpp" @@ -61,18 +83,24 @@ #include "inventorywindow.hpp" #include "bookpage.hpp" #include "itemview.hpp" -#include "fontloader.hpp" #include "videowidget.hpp" #include "backgroundimage.hpp" #include "itemwidget.hpp" +#include "screenfader.hpp" +#include "debugwindow.hpp" +#include "spellview.hpp" +#include "draganddrop.hpp" +#include "container.hpp" +#include "controllers.hpp" +#include "jailscreen.hpp" namespace MWGui { WindowManager::WindowManager( - const Compiler::Extensions& extensions, int fpsLevel, OEngine::Render::OgreRenderer *ogre, + const Compiler::Extensions& extensions, OEngine::Render::OgreRenderer *ogre, const std::string& logpath, const std::string& cacheDir, bool consoleOnlyScripts, - Translation::Storage& translationDataStorage, ToUTF8::FromType encoding) + Translation::Storage& translationDataStorage, ToUTF8::FromType encoding, bool exportFonts, const std::map& fallbackMap) : mConsoleOnlyScripts(consoleOnlyScripts) , mGuiManager(NULL) , mRendering(ogre) @@ -112,11 +140,19 @@ namespace MWGui , mCompanionWindow(NULL) , mVideoBackground(NULL) , mVideoWidget(NULL) + , mWerewolfFader(NULL) + , mBlindnessFader(NULL) + , mHitFader(NULL) + , mScreenFader(NULL) + , mDebugWindow(NULL) + , mJailScreen(NULL) , mTranslationDataStorage (translationDataStorage) , mCharGen(NULL) , mInputBlocker(NULL) , mCrosshairEnabled(Settings::Manager::getBool ("crosshair", "HUD")) , mSubtitlesEnabled(Settings::Manager::getBool ("subtitles", "GUI")) + , mHitFaderEnabled(Settings::Manager::getBool ("hit fader", "GUI")) + , mWerewolfOverlayEnabled(Settings::Manager::getBool ("werewolf overlay", "GUI")) , mHudEnabled(true) , mGuiEnabled(true) , mCursorVisible(true) @@ -134,19 +170,20 @@ namespace MWGui , mForceHidden(GW_None) , mAllowed(GW_ALL) , mRestAllowed(true) - , mShowFPSLevel(fpsLevel) , mFPS(0.0f) , mTriangleCount(0) , mBatchCount(0) , mCurrentModals() + , mFallbackMap(fallbackMap) { // Set up the GUI system mGuiManager = new OEngine::GUI::MyGUIManager(mRendering->getWindow(), mRendering->getScene(), false, logpath); - mGui = mGuiManager->getGui(); + + MyGUI::LanguageManager::getInstance().eventRequestTag = MyGUI::newDelegate(this, &WindowManager::onRetrieveTag); // Load fonts - FontLoader fontLoader (encoding); - fontLoader.loadAllFonts(); + Gui::FontLoader fontLoader (encoding); + fontLoader.loadAllFonts(exportFonts); //Register own widgets with MyGUI MyGUI::FactoryManager::getInstance().registerFactory("Widget"); @@ -155,13 +192,6 @@ namespace MWGui MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Widget"); - MyGUI::FactoryManager::getInstance().registerFactory("Widget"); - MyGUI::FactoryManager::getInstance().registerFactory("Widget"); - MyGUI::FactoryManager::getInstance().registerFactory("Widget"); - MyGUI::FactoryManager::getInstance().registerFactory("Widget"); - MyGUI::FactoryManager::getInstance().registerFactory("Widget"); - MyGUI::FactoryManager::getInstance().registerFactory("Widget"); - MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Widget"); @@ -169,20 +199,16 @@ namespace MWGui BookPage::registerMyGUIComponents (); ItemView::registerComponents(); ItemWidget::registerComponents(); + SpellView::registerComponents(); + Gui::registerAllWidgets(); - MyGUI::FactoryManager::getInstance().registerFactory("Controller"); + MyGUI::FactoryManager::getInstance().registerFactory("Controller"); + MyGUI::FactoryManager::getInstance().registerFactory("Controller"); MyGUI::FactoryManager::getInstance().registerFactory("Resource", "ResourceImageSetPointer"); MyGUI::ResourceManager::getInstance().load("core.xml"); - MyGUI::LanguageManager::getInstance().eventRequestTag = MyGUI::newDelegate(this, &WindowManager::onRetrieveTag); - - // Get size info from the Gui object - int w = MyGUI::RenderManager::getInstance().getViewSize().width; - int h = MyGUI::RenderManager::getInstance().getViewSize().height; - mLoadingScreen = new LoadingScreen(mRendering->getScene (), mRendering->getWindow ()); - mLoadingScreen->onResChange (w,h); //set up the hardware cursor manager mCursorManager = new SFO::SDLCursorManager(); @@ -192,7 +218,6 @@ namespace MWGui MyGUI::InputManager::getInstance().eventChangeKeyFocus += MyGUI::newDelegate(this, &WindowManager::onKeyFocusChanged); onCursorChange(MyGUI::PointerManager::getInstance().getDefaultPointer()); - //SDL_ShowCursor(false); mCursorManager->setEnabled(true); @@ -209,6 +234,13 @@ namespace MWGui mVideoWidget = mVideoBackground->createWidgetReal("ImageBox", 0,0,1,1, MyGUI::Align::Default); mVideoWidget->setNeedMouseFocus(true); mVideoWidget->setNeedKeyFocus(true); + + // Removes default MyGUI system clipboard implementation, which supports windows only + MyGUI::ClipboardManager::getInstance().eventClipboardChanged.clear(); + MyGUI::ClipboardManager::getInstance().eventClipboardRequested.clear(); + + MyGUI::ClipboardManager::getInstance().eventClipboardChanged += MyGUI::newDelegate(this, &WindowManager::onClipboardChanged); + MyGUI::ClipboardManager::getInstance().eventClipboardRequested += MyGUI::newDelegate(this, &WindowManager::onClipboardRequested); } void WindowManager::initUI() @@ -217,17 +249,11 @@ namespace MWGui int w = MyGUI::RenderManager::getInstance().getViewSize().width; int h = MyGUI::RenderManager::getInstance().getViewSize().height; - MyGUI::Widget* dragAndDropWidget = mGui->createWidgetT("Widget","",0,0,w,h,MyGUI::Align::Default,"DragAndDrop","DragAndDropWidget"); - dragAndDropWidget->setVisible(false); - mDragAndDrop = new DragAndDrop(); - mDragAndDrop->mIsOnDragAndDrop = false; - mDragAndDrop->mDraggedWidget = 0; - mDragAndDrop->mDragAndDropWidget = dragAndDropWidget; mRecharge = new Recharge(); mMenu = new MainMenu(w,h); - mMap = new MapWindow(mDragAndDrop, ""); + mMap = new MapWindow(mCustomMarkers, mDragAndDrop, ""); trackWindow(mMap, "map"); mStatsWindow = new StatsWindow(mDragAndDrop); trackWindow(mStatsWindow, "stats"); @@ -245,7 +271,7 @@ namespace MWGui trackWindow(mDialogueWindow, "dialogue"); mContainerWindow = new ContainerWindow(mDragAndDrop); trackWindow(mContainerWindow, "container"); - mHud = new HUD(w,h, mShowFPSLevel, mDragAndDrop); + mHud = new HUD(mCustomMarkers, Settings::Manager::getInt("fps", "HUD"), mDragAndDrop); mToolTips = new ToolTips(); mScrollWindow = new ScrollWindow(); mBookWindow = new BookWindow(); @@ -267,8 +293,21 @@ namespace MWGui mSoulgemDialog = new SoulgemDialog(mMessageBoxManager); mCompanionWindow = new CompanionWindow(mDragAndDrop, mMessageBoxManager); trackWindow(mCompanionWindow, "companion"); + mJailScreen = new JailScreen(); + + mWerewolfFader = new ScreenFader("textures\\werewolfoverlay.dds"); + mBlindnessFader = new ScreenFader("black.png"); + std::string hitFaderTexture = "textures\\bm_player_hit_01.dds"; + // fall back to player_hit_01.dds if bm_player_hit_01.dds is not available + // TODO: check if non-BM versions actually use player_hit_01.dds + if(!Ogre::ResourceGroupManager::getSingleton().resourceExistsInAnyGroup(hitFaderTexture)) + hitFaderTexture = "textures\\player_hit_01.dds"; + mHitFader = new ScreenFader(hitFaderTexture); + mScreenFader = new ScreenFader("black.png"); - mInputBlocker = mGui->createWidget("",0,0,w,h,MyGUI::Align::Default,"Overlay"); + mDebugWindow = new DebugWindow(); + + mInputBlocker = MyGUI::Gui::getInstance().createWidget("",0,0,w,h,MyGUI::Align::Stretch,"Overlay"); mHud->setVisible(mHudEnabled); @@ -321,6 +360,12 @@ namespace MWGui WindowManager::~WindowManager() { + MyGUI::LanguageManager::getInstance().eventRequestTag.clear(); + MyGUI::PointerManager::getInstance().eventChangeMousePointer.clear(); + MyGUI::InputManager::getInstance().eventChangeKeyFocus.clear(); + MyGUI::ClipboardManager::getInstance().eventClipboardChanged.clear(); + MyGUI::ClipboardManager::getInstance().eventClipboardRequested.clear(); + delete mConsole; delete mMessageBoxManager; delete mHud; @@ -354,9 +399,16 @@ namespace MWGui delete mMerchantRepair; delete mRepair; delete mSoulgemDialog; - delete mCursorManager; delete mRecharge; delete mCompanionWindow; + delete mHitFader; + delete mWerewolfFader; + delete mScreenFader; + delete mBlindnessFader; + delete mDebugWindow; + delete mJailScreen; + + delete mCursorManager; cleanupGarbage(); @@ -420,8 +472,10 @@ namespace MWGui mInventoryWindow->setTrading(false); mRecharge->setVisible(false); mVideoBackground->setVisible(false); + mJailScreen->setVisible(false); mHud->setVisible(mHudEnabled && mGuiEnabled); + mToolTips->setVisible(mGuiEnabled); bool gameMode = !isGuiMode(); @@ -564,18 +618,16 @@ namespace MWGui case GM_Journal: mJournal->setVisible(true); break; - case GM_LoadingWallpaper: - mHud->setVisible(false); - setCursorVisible(false); + case GM_Jail: + mJailScreen->setVisible(true); break; + case GM_LoadingWallpaper: case GM_Loading: - // Show the pinned windows - mMap->setVisible(mMap->pinned() && !(mForceHidden & GW_Map)); - mStatsWindow->setVisible(mStatsWindow->pinned() && !(mForceHidden & GW_Stats)); - mInventoryWindow->setVisible(mInventoryWindow->pinned() && !(mForceHidden & GW_Inventory)); - mSpellWindow->setVisible(mSpellWindow->pinned() && !(mForceHidden & GW_Magic)); - - setCursorVisible(false); + // Don't need to show anything here - GM_LoadingWallpaper covers everything else anyway, + // GM_Loading uses a texture of the last rendered frame so everything previously visible will be rendered. + mHud->setVisible(false); + mToolTips->setVisible(false); + setCursorVisible(mMessageBoxManager && mMessageBoxManager->isInteractiveMessageBox()); break; default: // Unsupported mode, switch back to game @@ -773,18 +825,31 @@ namespace MWGui } } - void WindowManager::messageBox (const std::string& message, const std::vector& buttons, enum MWGui::ShowInDialogueMode showInDialogueMode) + void WindowManager::interactiveMessageBox(const std::string &message, const std::vector &buttons, bool block) { - if (buttons.empty()) { - /* If there are no buttons, and there is a dialogue window open, messagebox goes to the dialogue window */ - if (getMode() == GM_Dialogue && showInDialogueMode != MWGui::ShowInDialogueMode_Never) { - mDialogueWindow->addMessageBox(MyGUI::LanguageManager::getInstance().replaceTags(message)); - } else if (showInDialogueMode != MWGui::ShowInDialogueMode_Only) { - mMessageBoxManager->createMessageBox(message); + mMessageBoxManager->createInteractiveMessageBox(message, buttons); + MWBase::Environment::get().getInputManager()->changeInputMode(isGuiMode()); + updateVisible(); + + if (block) + { + while (mMessageBoxManager->readPressedButton(false) == -1 + && !MWBase::Environment::get().getStateManager()->hasQuitRequest()) + { + mMessageBoxManager->onFrame(0.f); + MWBase::Environment::get().getInputManager()->update(0, true, false); + + mRendering->getWindow()->update(); } - } else { - mMessageBoxManager->createInteractiveMessageBox(message, buttons); - MWBase::Environment::get().getInputManager()->changeInputMode(isGuiMode()); + } + } + + void WindowManager::messageBox (const std::string& message, enum MWGui::ShowInDialogueMode showInDialogueMode) + { + if (getMode() == GM_Dialogue && showInDialogueMode != MWGui::ShowInDialogueMode_Never) { + mDialogueWindow->addMessageBox(MyGUI::LanguageManager::getInstance().replaceTags(message)); + } else if (showInDialogueMode != MWGui::ShowInDialogueMode_Only) { + mMessageBoxManager->createMessageBox(message); } } @@ -845,7 +910,6 @@ namespace MWGui mHud->onFrame(frameDuration); mTrainingWindow->onFrame (frameDuration); - mTradeWindow->onFrame(frameDuration); mTrainingWindow->checkReferenceAvailable(); mDialogueWindow->checkReferenceAvailable(); @@ -857,6 +921,14 @@ namespace MWGui mCompanionWindow->checkReferenceAvailable(); mConsole->checkReferenceAvailable(); mCompanionWindow->onFrame(); + mJailScreen->onFrame(frameDuration); + + mWerewolfFader->update(frameDuration); + mBlindnessFader->update(frameDuration); + mHitFader->update(frameDuration); + mScreenFader->update(frameDuration); + + mDebugWindow->onFrame(frameDuration); } void WindowManager::changeCell(MWWorld::CellStore* cell) @@ -872,9 +944,6 @@ namespace MWGui mMap->addVisitedLocation ("#{sCell=" + name + "}", cell->getCell()->getGridX (), cell->getCell()->getGridY ()); mMap->cellExplored (cell->getCell()->getGridX(), cell->getCell()->getGridY()); - - mMap->setCellPrefix("Cell"); - mHud->setCellPrefix("Cell"); } else { @@ -892,19 +961,26 @@ namespace MWGui void WindowManager::setActiveMap(int x, int y, bool interior) { + if (!interior) + { + mMap->setCellPrefix("Cell"); + mHud->setCellPrefix("Cell"); + } + mMap->setActiveCell(x,y, interior); mHud->setActiveCell(x,y, interior); } - void WindowManager::setPlayerPos(const float x, const float y) + void WindowManager::setPlayerPos(int cellX, int cellY, const float x, const float y) { - mMap->setPlayerPos(x,y); - mHud->setPlayerPos(x,y); + mMap->setPlayerPos(cellX, cellY, x, y); + mHud->setPlayerPos(cellX, cellY, x, y); } void WindowManager::setPlayerDir(const float x, const float y) { mMap->setPlayerDir(x,y); + mMap->setGlobalMapPlayerDir(x, y); mHud->setPlayerDir(x,y); } @@ -983,10 +1059,14 @@ namespace MWGui std::string tokenToFind = "sCell="; size_t tokenLength = tokenToFind.length(); - if (tag.substr(0, tokenLength) == tokenToFind) + if (tag.compare(0, tokenLength, tokenToFind) == 0) { _result = mTranslationDataStorage.translateCellName(tag.substr(tokenLength)); } + else if (Gui::replaceTag(tag, _result, mFallbackMap)) + { + return; + } else { const ESM::GameSetting *setting = @@ -1018,7 +1098,6 @@ namespace MWGui { sizeVideo(x, y); mGuiManager->windowResized(); - mLoadingScreen->onResChange (x,y); if (!mHud) return; // UI not initialized yet @@ -1032,7 +1111,6 @@ namespace MWGui it->first->setSize(size); } - mHud->onResChange(x, y); mConsole->onResChange(x, y); mMenu->onResChange(x, y); mSettingsWindow->center(); @@ -1041,8 +1119,6 @@ namespace MWGui mBookWindow->center(); mQuickKeysMenu->center(); mSpellBuyingWindow->center(); - mDragAndDrop->mDragAndDropWidget->setSize(MyGUI::IntSize(x, y)); - mInputBlocker->setSize(MyGUI::IntSize(x,y)); } void WindowManager::pushGuiMode(GuiMode mode) @@ -1121,6 +1197,12 @@ namespace MWGui updateVisible(); } + void WindowManager::goToJail(int days) + { + pushGuiMode(MWGui::GM_Jail); + mJailScreen->goToJail(days); + } + void WindowManager::setSelectedSpell(const std::string& spellId, int successChancePercent) { mSelectedSpell = spellId; @@ -1204,8 +1286,6 @@ namespace MWGui mBatchCount = batchCount; } - MyGUI::Gui* WindowManager::getGui() const { return mGui; } - MWGui::DialogueWindow* WindowManager::getDialogueWindow() { return mDialogueWindow; } MWGui::ContainerWindow* WindowManager::getContainerWindow() { return mContainerWindow; } MWGui::InventoryWindow* WindowManager::getInventoryWindow() { return mInventoryWindow; } @@ -1347,12 +1427,6 @@ namespace MWGui return mSubtitlesEnabled; } - void WindowManager::toggleHud () - { - mHudEnabled = !mHudEnabled; - mHud->setVisible (mHudEnabled); - } - bool WindowManager::toggleGui() { mGuiEnabled = !mGuiEnabled; @@ -1432,6 +1506,8 @@ namespace MWGui void WindowManager::showSoulgemDialog(MWWorld::Ptr item) { mSoulgemDialog->show(item); + MWBase::Environment::get().getInputManager()->changeInputMode(isGuiMode()); + updateVisible(); } void WindowManager::frameStarted (float dt) @@ -1443,8 +1519,16 @@ namespace MWGui void WindowManager::updatePlayer() { mInventoryWindow->updatePlayer(); + + const MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); + if (player.getClass().getNpcStats(player).isWerewolf()) + { + setWerewolfOverlay(true); + forceHide((GuiWindow)(MWGui::GW_Inventory | MWGui::GW_Magic)); + } } + // Remove this method for MyGUI 3.2.2 void WindowManager::setKeyFocusWidget(MyGUI::Widget *widget) { if (widget == NULL) @@ -1531,6 +1615,12 @@ namespace MWGui mSelectedSpell.clear(); + mCustomMarkers.clear(); + + mForceHidden = GW_None; + + setWerewolfOverlay(false); + mGuiModes.clear(); MWBase::Environment::get().getInputManager()->changeInputMode(false); updateVisible(); @@ -1541,18 +1631,23 @@ namespace MWGui mMap->write(writer, progress); mQuickKeysMenu->write(writer); - progress.increaseProgress(); if (!mSelectedSpell.empty()) { writer.startRecord(ESM::REC_ASPL); writer.writeHNString("ID__", mSelectedSpell); writer.endRecord(ESM::REC_ASPL); - progress.increaseProgress(); + } + + for (std::vector::const_iterator it = mCustomMarkers.begin(); it != mCustomMarkers.end(); ++it) + { + writer.startRecord(ESM::REC_MARK); + (*it).save(writer); + writer.endRecord(ESM::REC_MARK); } } - void WindowManager::readRecord(ESM::ESMReader &reader, int32_t type) + void WindowManager::readRecord(ESM::ESMReader &reader, uint32_t type) { if (type == ESM::REC_GMAP) mMap->readRecord(reader, type); @@ -1563,12 +1658,19 @@ namespace MWGui reader.getSubNameIs("ID__"); mSelectedSpell = reader.getHString(); } + else if (type == ESM::REC_MARK) + { + ESM::CustomMarker marker; + marker.load(reader); + mCustomMarkers.addMarker(marker, false); + } } int WindowManager::countSavedGameRecords() const { return 1 // Global map + 1 // QuickKeysMenu + + mCustomMarkers.size() + (!mSelectedSpell.empty() ? 1 : 0); } @@ -1611,6 +1713,10 @@ namespace MWGui bool cursorWasVisible = mCursorVisible; setCursorVisible(false); + if (mVideoWidget->hasAudioStream()) + MWBase::Environment::get().getSoundManager()->pauseSounds( + MWBase::SoundManager::Play_TypeMask&(~MWBase::SoundManager::Play_TypeMovie)); + while (mVideoWidget->update() && !MWBase::Environment::get().getStateManager()->hasQuitRequest()) { MWBase::Environment::get().getInputManager()->update(0, true, false); @@ -1619,6 +1725,8 @@ namespace MWGui } mVideoWidget->stop(); + MWBase::Environment::get().getSoundManager()->resumeSounds(); + setCursorVisible(cursorWasVisible); // Restore normal rendering @@ -1631,20 +1739,14 @@ namespace MWGui void WindowManager::sizeVideo(int screenWidth, int screenHeight) { // Use black bars to correct aspect ratio + bool stretch = Settings::Manager::getBool("stretch menu background", "GUI"); mVideoBackground->setSize(screenWidth, screenHeight); - - double imageaspect = static_cast(mVideoWidget->getVideoWidth())/mVideoWidget->getVideoHeight(); - - int leftPadding = std::max(0.0, (screenWidth - screenHeight * imageaspect) / 2); - int topPadding = std::max(0.0, (screenHeight - screenWidth / imageaspect) / 2); - - mVideoWidget->setCoord(leftPadding, topPadding, - screenWidth - leftPadding*2, screenHeight - topPadding*2); + mVideoWidget->autoResize(stretch); } WindowModal* WindowManager::getCurrentModal() const { - if(mCurrentModals.size() > 0) + if(!mCurrentModals.empty()) return mCurrentModals.top(); else return NULL; @@ -1654,7 +1756,7 @@ namespace MWGui { // Only remove the top if it matches the current pointer. A lot of things hide their visibility before showing it, //so just popping the top would cause massive issues. - if(mCurrentModals.size() > 0) + if(!mCurrentModals.empty()) if(input == mCurrentModals.top()) mCurrentModals.pop(); } @@ -1687,4 +1789,89 @@ namespace MWGui updateVisible(); } + + void WindowManager::fadeScreenIn(const float time, bool clearQueue) + { + if (clearQueue) + mScreenFader->clearQueue(); + mScreenFader->fadeOut(time); + } + + void WindowManager::fadeScreenOut(const float time, bool clearQueue) + { + if (clearQueue) + mScreenFader->clearQueue(); + mScreenFader->fadeIn(time); + } + + void WindowManager::fadeScreenTo(const int percent, const float time, bool clearQueue) + { + if (clearQueue) + mScreenFader->clearQueue(); + mScreenFader->fadeTo(percent, time); + } + + void WindowManager::setBlindness(const int percent) + { + mBlindnessFader->notifyAlphaChanged(percent / 100.f); + } + + void WindowManager::activateHitOverlay(bool interrupt) + { + if (!mHitFaderEnabled) + return; + + if (!interrupt && !mHitFader->isEmpty()) + return; + + mHitFader->clearQueue(); + mHitFader->fadeTo(100, 0.0f); + mHitFader->fadeTo(0, 0.5f); + } + + void WindowManager::setWerewolfOverlay(bool set) + { + if (!mWerewolfOverlayEnabled) + return; + + mWerewolfFader->notifyAlphaChanged(set ? 1.0f : 0.0f); + } + + void WindowManager::onClipboardChanged(const std::string &_type, const std::string &_data) + { + if (_type == "Text") + SDL_SetClipboardText(MyGUI::TextIterator::getOnlyText(MyGUI::UString(_data)).asUTF8().c_str()); + } + + void WindowManager::onClipboardRequested(const std::string &_type, std::string &_data) + { + if (_type != "Text") + return; + char* text=0; + text = SDL_GetClipboardText(); + if (text) + { + // MyGUI's clipboard might still have color information, to retain that information, only set the new text + // if it actually changed (clipboard inserted by an external application) + if (MyGUI::TextIterator::getOnlyText(_data) != text) + _data = text; + } + SDL_free(text); + } + + void WindowManager::toggleDebugWindow() + { + mDebugWindow->setVisible(!mDebugWindow->isVisible()); + } + + void WindowManager::cycleSpell(bool next) + { + mSpellWindow->cycle(next); + } + + void WindowManager::cycleWeapon(bool next) + { + mInventoryWindow->cycle(next); + } + } diff --git a/apps/openmw/mwgui/windowmanagerimp.hpp b/apps/openmw/mwgui/windowmanagerimp.hpp index 8093d637e..735b02d2d 100644 --- a/apps/openmw/mwgui/windowmanagerimp.hpp +++ b/apps/openmw/mwgui/windowmanagerimp.hpp @@ -5,13 +5,17 @@ This class owns and controls all the MW specific windows in the GUI. It can enable/disable Gui mode, and is responsible for sending and retrieving information from the Gui. - - MyGUI should be initialized separately before creating instances of - this class. **/ +#include + #include "../mwbase/windowmanager.hpp" +#include +#include + +#include "mapwindow.hpp" + #include #include @@ -77,7 +81,6 @@ namespace MWGui class SpellCreationDialog; class EnchantingDialog; class TrainingWindow; - class Cursor; class SpellIcons; class MerchantRepair; class Repair; @@ -86,6 +89,9 @@ namespace MWGui class CompanionWindow; class VideoWidget; class WindowModal; + class ScreenFader; + class DebugWindow; + class JailScreen; class WindowManager : public MWBase::WindowManager { @@ -93,10 +99,10 @@ namespace MWGui typedef std::pair Faction; typedef std::vector FactionList; - WindowManager(const Compiler::Extensions& extensions, int fpsLevel, + WindowManager(const Compiler::Extensions& extensions, OEngine::Render::OgreRenderer *mOgre, const std::string& logpath, const std::string& cacheDir, bool consoleOnlyScripts, - Translation::Storage& translationDataStorage, ToUTF8::FromType encoding); + Translation::Storage& translationDataStorage, ToUTF8::FromType encoding, bool exportFonts, const std::map& fallbackMap); virtual ~WindowManager(); void initUI(); @@ -124,6 +130,8 @@ namespace MWGui virtual void popGuiMode(); virtual void removeGuiMode(GuiMode mode); ///< can be anywhere in the stack + virtual void goToJail(int days); + virtual GuiMode getMode() const; virtual bool containsMode(GuiMode mode) const; @@ -158,8 +166,6 @@ namespace MWGui virtual MWGui::SpellWindow* getSpellWindow(); virtual MWGui::Console* getConsole(); - virtual MyGUI::Gui* getGui() const; - virtual void wmUpdateFps(float fps, unsigned int triangleCount, unsigned int batchCount); ///< Set value for the given ID. @@ -181,7 +187,7 @@ namespace MWGui virtual void updateSkillArea(); ///< update display of skills, factions, birth sign, reputation and bounty virtual void changeCell(MWWorld::CellStore* cell); ///< change the active cell - virtual void setPlayerPos(const float x, const float y); ///< set player position in map space + virtual void setPlayerPos(int cellX, int cellY, const float x, const float y); ///< set player position in map space virtual void setPlayerDir(const float x, const float y); ///< set player view direction in map space virtual void setFocusObject(const MWWorld::Ptr& focus); @@ -221,7 +227,6 @@ namespace MWGui virtual void showCrosshair(bool show); virtual bool getSubtitlesEnabled(); - virtual void toggleHud(); /// Turn visibility of *all* GUI elements on or off (HUD and all windows, except the console) virtual bool toggleGui(); @@ -238,9 +243,12 @@ namespace MWGui ///Gracefully attempts to exit the topmost GUI mode virtual void exitCurrentGuiMode(); - virtual void messageBox (const std::string& message, const std::vector& buttons = std::vector(), enum MWGui::ShowInDialogueMode showInDialogueMode = MWGui::ShowInDialogueMode_IfPossible); + virtual void messageBox (const std::string& message, enum MWGui::ShowInDialogueMode showInDialogueMode = MWGui::ShowInDialogueMode_IfPossible); virtual void staticMessageBox(const std::string& message); virtual void removeStaticMessageBox(); + virtual void interactiveMessageBox (const std::string& message, + const std::vector& buttons = std::vector(), bool block=false); + virtual int readPressedButton (); ///< returns the index of the pressed button or -1 if no button was pressed (->MessageBoxmanager->InteractiveMessageBox) virtual void onFrame (float frameDuration); @@ -303,7 +311,7 @@ namespace MWGui virtual void clear(); virtual void write (ESM::ESMWriter& writer, Loading::Listener& progress); - virtual void readRecord (ESM::ESMReader& reader, int32_t type); + virtual void readRecord (ESM::ESMReader& reader, uint32_t type); virtual int countSavedGameRecords() const; /// Does the current stack of GUI-windows permit saving? @@ -324,6 +332,25 @@ namespace MWGui virtual void pinWindow (MWGui::GuiWindow window); + /// Fade the screen in, over \a time seconds + virtual void fadeScreenIn(const float time, bool clearQueue); + /// Fade the screen out to black, over \a time seconds + virtual void fadeScreenOut(const float time, bool clearQueue); + /// Fade the screen to a specified percentage of black, over \a time seconds + virtual void fadeScreenTo(const int percent, const float time, bool clearQueue); + /// Darken the screen to a specified percentage + virtual void setBlindness(const int percent); + + virtual void activateHitOverlay(bool interrupt); + virtual void setWerewolfOverlay(bool set); + + virtual void toggleDebugWindow(); + + /// Cycle to next or previous spell + virtual void cycleSpell(bool next); + /// Cycle to next or previous weapon + virtual void cycleWeapon(bool next); + private: bool mConsoleOnlyScripts; @@ -335,6 +362,9 @@ namespace MWGui std::stack mCurrentModals; + // Markers placed manually by the player. Must be shared between both map views (the HUD map and the map window). + CustomMarkerCollection mCustomMarkers; + OEngine::GUI::MyGUIManager *mGuiManager; OEngine::Render::OgreRenderer *mRendering; HUD *mHud; @@ -373,9 +403,14 @@ namespace MWGui CompanionWindow* mCompanionWindow; MyGUI::ImageBox* mVideoBackground; VideoWidget* mVideoWidget; + ScreenFader* mWerewolfFader; + ScreenFader* mBlindnessFader; + ScreenFader* mHitFader; + ScreenFader* mScreenFader; + DebugWindow* mDebugWindow; + JailScreen* mJailScreen; Translation::Storage& mTranslationDataStorage; - Cursor* mSoftwareCursor; CharacterCreation* mCharGen; @@ -383,6 +418,8 @@ namespace MWGui bool mCrosshairEnabled; bool mSubtitlesEnabled; + bool mHitFaderEnabled; + bool mWerewolfOverlayEnabled; bool mHudEnabled; bool mGuiEnabled; bool mCursorVisible; @@ -419,14 +456,21 @@ namespace MWGui void updateVisible(); // Update visibility of all windows based on mode, shown and allowed settings - int mShowFPSLevel; float mFPS; unsigned int mTriangleCount; unsigned int mBatchCount; + std::map mFallbackMap; + /** - * Called when MyGUI tries to retrieve a tag. This usually corresponds to a GMST string, - * so this method will retrieve the GMST with the name \a _tag and place the result in \a _result + * Called when MyGUI tries to retrieve a tag's value. Tags must be denoted in #{tag} notation and will be replaced upon setting a user visible text/property. + * Supported syntax: + * #{GMSTName}: retrieves String value of the GMST called GMSTName + * #{sCell=CellID}: retrieves translated name of the given CellID (used only by some Morrowind localisations, in others cell ID is == cell name) + * #{fontcolour=FontColourName}: retrieves the value of the fallback setting "FontColor_color_" from openmw.cfg, + * in the format "r g b a", float values in range 0-1. Useful for "Colour" and "TextColour" properties in skins. + * #{fontcolourhtml=FontColourName}: retrieves the value of the fallback setting "FontColor_color_" from openmw.cfg, + * in the format "#xxxxxx" where x are hexadecimal numbers. Useful in an EditBox's caption to change the color of following text. */ void onRetrieveTag(const MyGUI::UString& _tag, MyGUI::UString& _result); @@ -437,6 +481,9 @@ namespace MWGui void onVideoKeyPressed(MyGUI::Widget *_sender, MyGUI::KeyCode _key, MyGUI::Char _char); void sizeVideo(int screenWidth, int screenHeight); + + void onClipboardChanged(const std::string& _type, const std::string& _data); + void onClipboardRequested(const std::string& _type, std::string& _data); }; } diff --git a/apps/openmw/mwgui/windowpinnablebase.cpp b/apps/openmw/mwgui/windowpinnablebase.cpp index 919d315f2..a6984b5a4 100644 --- a/apps/openmw/mwgui/windowpinnablebase.cpp +++ b/apps/openmw/mwgui/windowpinnablebase.cpp @@ -1,5 +1,7 @@ #include "windowpinnablebase.hpp" +#include + #include "exposedwindow.hpp" namespace MWGui @@ -7,14 +9,28 @@ namespace MWGui WindowPinnableBase::WindowPinnableBase(const std::string& parLayout) : WindowBase(parLayout), mPinned(false) { - ExposedWindow* window = static_cast(mMainWidget); + ExposedWindow* window = mMainWidget->castType(); mPinButton = window->getSkinWidget ("Button"); - mPinButton->eventMouseButtonClick += MyGUI::newDelegate(this, &WindowPinnableBase::onPinButtonClicked); + mPinButton->eventMouseButtonPressed += MyGUI::newDelegate(this, &WindowPinnableBase::onPinButtonPressed); + + MyGUI::Button* button = NULL; + MyGUI::VectorWidgetPtr widgets = window->getSkinWidgetsByName("Action"); + for (MyGUI::VectorWidgetPtr::iterator it = widgets.begin(); it != widgets.end(); ++it) + { + if ((*it)->isUserString("HideWindowOnDoubleClick")) + button = (*it)->castType(); + } + + if (button) + button->eventMouseButtonDoubleClick += MyGUI::newDelegate(this, &WindowPinnableBase::onDoubleClick); } - void WindowPinnableBase::onPinButtonClicked(MyGUI::Widget* _sender) + void WindowPinnableBase::onPinButtonPressed(MyGUI::Widget* _sender, int left, int top, MyGUI::MouseButton id) { + if (id != MyGUI::MouseButton::Left) + return; + mPinned = !mPinned; if (mPinned) @@ -25,10 +41,15 @@ namespace MWGui onPinToggled(); } + void WindowPinnableBase::onDoubleClick(MyGUI::Widget *_sender) + { + onTitleDoubleClicked(); + } + void WindowPinnableBase::setPinned(bool pinned) { if (pinned != mPinned) - onPinButtonClicked(mPinButton); + onPinButtonPressed(mPinButton, 0, 0, MyGUI::MouseButton::Left); } void WindowPinnableBase::setPinButtonVisible(bool visible) diff --git a/apps/openmw/mwgui/windowpinnablebase.hpp b/apps/openmw/mwgui/windowpinnablebase.hpp index 3aad60988..c085bebf2 100644 --- a/apps/openmw/mwgui/windowpinnablebase.hpp +++ b/apps/openmw/mwgui/windowpinnablebase.hpp @@ -16,10 +16,12 @@ namespace MWGui void setPinButtonVisible(bool visible); private: - void onPinButtonClicked(MyGUI::Widget* _sender); + void onPinButtonPressed(MyGUI::Widget* _sender, int left, int top, MyGUI::MouseButton id); + void onDoubleClick(MyGUI::Widget* _sender); protected: virtual void onPinToggled() = 0; + virtual void onTitleDoubleClicked() = 0; MyGUI::Widget* mPinButton; bool mPinned; diff --git a/apps/openmw/mwinput/inputmanagerimp.cpp b/apps/openmw/mwinput/inputmanagerimp.cpp index 067ad72bb..4212c562b 100644 --- a/apps/openmw/mwinput/inputmanagerimp.cpp +++ b/apps/openmw/mwinput/inputmanagerimp.cpp @@ -2,6 +2,7 @@ #include #include +#include #include @@ -11,6 +12,8 @@ #include #include +#include + #include #include "../engine.hpp" @@ -32,6 +35,8 @@ #include "../mwgui/windowbase.hpp" +#include + using namespace ICS; namespace @@ -97,7 +102,8 @@ namespace MWInput { InputManager::InputManager(OEngine::Render::OgreRenderer &ogre, OMW::Engine& engine, - const std::string& userFile, bool userFileExists, bool grab) + const std::string& userFile, bool userFileExists, + const std::string& controllerBindingsFile, bool grab) : mOgre(ogre) , mPlayer(NULL) , mEngine(engine) @@ -120,6 +126,9 @@ namespace MWInput , mAlwaysRunActive(Settings::Manager::getBool("always run", "Input")) , mAttemptJump(false) , mControlsDisabled(false) + , mJoystickLastUsed(false) + , mDetectingKeyboard(false) + , mFakeDeviceID(1) { Ogre::RenderWindow* window = ogre.getWindow (); @@ -128,13 +137,14 @@ namespace MWInput mInputManager->setMouseEventCallback (this); mInputManager->setKeyboardEventCallback (this); mInputManager->setWindowEventCallback(this); + mInputManager->setControllerEventCallback(this); std::string file = userFileExists ? userFile : ""; mInputBinder = new ICS::InputControlSystem(file, true, this, NULL, A_Last); - adjustMouseRegion (window->getWidth(), window->getHeight()); loadKeyDefaults(); + loadControllerDefaults(); for (int i = 0; i < A_Last; ++i) { @@ -148,6 +158,32 @@ namespace MWInput mControlSwitch["playermagic"] = true; mControlSwitch["playerviewswitch"] = true; mControlSwitch["vanitymode"] = true; + + /* Joystick Init */ + + //Load controller mappings +#if SDL_VERSION_ATLEAST(2,0,2) + if(controllerBindingsFile!="") + { + SDL_GameControllerAddMappingsFromFile(controllerBindingsFile.c_str()); + } +#endif + + //Open all presently connected sticks + int numSticks = SDL_NumJoysticks(); + for(int i = 0; i < numSticks; i++) + { + if(SDL_IsGameController(i)) + { + SDL_ControllerDeviceEvent evt; + evt.which = i; + controllerAdded(mFakeDeviceID, evt); + } + else + { + //ICS_LOG(std::string("Unusable controller plugged in: ")+SDL_JoystickNameForIndex(i)); + } + } } void InputManager::clear() @@ -190,14 +226,33 @@ namespace MWInput int action = channel->getNumber(); - if (action == A_Use) + if((previousValue == 1 || previousValue == 0) && (currentValue==1 || currentValue==0)) + { + //Is a normal button press, so don't change it at all + } + //Otherwise only trigger button presses as they go through specific points + else if(previousValue >= .8 && currentValue < .8) + { + currentValue = 0.0; + previousValue = 1.0; + } + else if(previousValue <= .6 && currentValue > .6) + { + currentValue = 1.0; + previousValue = 0.0; + } + else { - mPlayer->getPlayer().getClass().getCreatureStats(mPlayer->getPlayer()).setAttackingOrSpell(currentValue); + //If it's not switching between those values, ignore the channel change. + return; } - if (action == A_Jump) + if (mControlSwitch["playercontrols"]) { - mAttemptJump = (currentValue == 1.0 && previousValue == 0.0); + if (action == A_Use) + mPlayer->getPlayer().getClass().getCreatureStats(mPlayer->getPlayer()).setAttackingOrSpell(currentValue); + else if (action == A_Jump) + mAttemptJump = (currentValue == 1.0 && previousValue == 0.0); } if (currentValue == 1) @@ -221,7 +276,6 @@ namespace MWInput break; case A_Activate: resetIdleTime(); - if (!MWBase::Environment::get().getWindowManager()->isGuiMode()) activate(); break; @@ -279,32 +333,33 @@ namespace MWInput case A_ToggleHUD: MWBase::Environment::get().getWindowManager()->toggleGui(); break; + case A_ToggleDebug: + MWBase::Environment::get().getWindowManager()->toggleDebugWindow(); + break; case A_QuickSave: quickSave(); break; case A_QuickLoad: quickLoad(); break; + case A_CycleSpellLeft: + MWBase::Environment::get().getWindowManager()->cycleSpell(false); + break; + case A_CycleSpellRight: + MWBase::Environment::get().getWindowManager()->cycleSpell(true); + break; + case A_CycleWeaponLeft: + MWBase::Environment::get().getWindowManager()->cycleWeapon(false); + break; + case A_CycleWeaponRight: + MWBase::Environment::get().getWindowManager()->cycleWeapon(true); + break; } } } - void InputManager::update(float dt, bool disableControls, bool disableEvents) + void InputManager::updateCursorMode() { - mControlsDisabled = disableControls; - - mInputManager->setMouseVisible(MWBase::Environment::get().getWindowManager()->getCursorVisible()); - - mInputManager->capture(disableEvents); - // inject some fake mouse movement to force updating MyGUI's widget states - MyGUI::InputManager::getInstance().injectMouseMove( int(mMouseX), int(mMouseY), mMouseWheel); - - if (mControlsDisabled) - return; - - // update values of channels (as a result of pressed keys) - mInputBinder->update(dt); - bool grab = !MWBase::Environment::get().getWindowManager()->containsMode(MWGui::GM_MainMenu) && MWBase::Environment::get().getWindowManager()->getMode() != MWGui::GM_Console; @@ -324,6 +379,69 @@ namespace MWInput { mInputManager->warpMouse(mMouseX, mMouseY); } + } + + void InputManager::update(float dt, bool disableControls, bool disableEvents) + { + mControlsDisabled = disableControls; + + mInputManager->setMouseVisible(MWBase::Environment::get().getWindowManager()->getCursorVisible()); + + mInputManager->capture(disableEvents); + // inject some fake mouse movement to force updating MyGUI's widget states + MyGUI::InputManager::getInstance().injectMouseMove( int(mMouseX), int(mMouseY), mMouseWheel); + + if (mControlsDisabled) + { + updateCursorMode(); + return; + } + + // update values of channels (as a result of pressed keys) + mInputBinder->update(dt); + + updateCursorMode(); + + if(mJoystickLastUsed) + { + if (mGuiCursorEnabled) + { + float xAxis = mInputBinder->getChannel(A_MoveLeftRight)->getValue()*2.0f-1.0f; + float yAxis = mInputBinder->getChannel(A_MoveForwardBackward)->getValue()*2.0f-1.0f; + float zAxis = mInputBinder->getChannel(A_LookUpDown)->getValue()*2.0f-1.0f; + const MyGUI::IntSize& viewSize = MyGUI::RenderManager::getInstance().getViewSize(); + + // We keep track of our own mouse position, so that moving the mouse while in + // game mode does not move the position of the GUI cursor + mMouseX += xAxis * dt * 1500.0f; + mMouseY += yAxis * dt * 1500.0f; + mMouseWheel -= zAxis * dt * 1500.0f; + + mMouseX = std::max(0.f, std::min(mMouseX, float(viewSize.width))); + mMouseY = std::max(0.f, std::min(mMouseY, float(viewSize.height))); + + MyGUI::InputManager::getInstance().injectMouseMove( mMouseX, mMouseY, mMouseWheel); + mInputManager->warpMouse(mMouseX, mMouseY); + } + if (mMouseLookEnabled) + { + float xAxis = mInputBinder->getChannel(A_LookLeftRight)->getValue()*2.0f-1.0f; + float yAxis = mInputBinder->getChannel(A_LookUpDown)->getValue()*2.0f-1.0f; + resetIdleTime(); + + float rot[3]; + rot[0] = yAxis * (dt * 100.0f) * 10.0f * mCameraSensitivity * (1.0f/256.f) * (mInvertY ? -1 : 1) * mCameraYMultiplier; + rot[1] = 0.0f; + rot[2] = xAxis * (dt * 100.0f) * 10.0f * mCameraSensitivity * (1.0f/256.f); + + // Only actually turn player when we're not in vanity mode + if(!MWBase::Environment::get().getWorld()->vanityRotateCamera(rot)) + { + mPlayer->yaw(rot[2]); + mPlayer->pitch(rot[0]); + } + } + } // Disable movement in Gui mode if (!(MWBase::Environment::get().getWindowManager()->isGuiMode() @@ -334,34 +452,74 @@ namespace MWInput if (mControlSwitch["playercontrols"]) { bool triedToMove = false; - if (actionIsActive(A_MoveLeft)) + bool isRunning = false; + if(mJoystickLastUsed) { - triedToMove = true; - mPlayer->setLeftRight (-1); - } - else if (actionIsActive(A_MoveRight)) - { - triedToMove = true; - mPlayer->setLeftRight (1); - } + float xAxis = mInputBinder->getChannel(A_MoveLeftRight)->getValue(); + float yAxis = mInputBinder->getChannel(A_MoveForwardBackward)->getValue(); + if (xAxis < .5) + { + triedToMove = true; + mPlayer->setLeftRight (-1); + } + else if (xAxis > .5) + { + triedToMove = true; + mPlayer->setLeftRight (1); + } - if (actionIsActive(A_MoveForward)) - { - triedToMove = true; - mPlayer->setAutoMove (false); - mPlayer->setForwardBackward (1); + if (yAxis < .5) + { + triedToMove = true; + mPlayer->setAutoMove (false); + mPlayer->setForwardBackward (1); + } + else if (yAxis > .5) + { + triedToMove = true; + mPlayer->setAutoMove (false); + mPlayer->setForwardBackward (-1); + } + + else if(mPlayer->getAutoMove()) + { + triedToMove = true; + mPlayer->setForwardBackward (1); + } + isRunning = xAxis > .75 || xAxis < .25 || yAxis > .75 || yAxis < .25; + if(triedToMove) resetIdleTime(); } - else if (actionIsActive(A_MoveBackward)) + else { - triedToMove = true; - mPlayer->setAutoMove (false); - mPlayer->setForwardBackward (-1); - } + if (actionIsActive(A_MoveLeft)) + { + triedToMove = true; + mPlayer->setLeftRight (-1); + } + else if (actionIsActive(A_MoveRight)) + { + triedToMove = true; + mPlayer->setLeftRight (1); + } - else if(mPlayer->getAutoMove()) - { - triedToMove = true; - mPlayer->setForwardBackward (1); + if (actionIsActive(A_MoveForward)) + { + triedToMove = true; + mPlayer->setAutoMove (false); + mPlayer->setForwardBackward (1); + } + else if (actionIsActive(A_MoveBackward)) + { + triedToMove = true; + mPlayer->setAutoMove (false); + mPlayer->setForwardBackward (-1); + } + + else if(mPlayer->getAutoMove()) + { + triedToMove = true; + mPlayer->setForwardBackward (1); + } } mPlayer->setSneak(actionIsActive(A_Sneak)); @@ -370,9 +528,10 @@ namespace MWInput { mPlayer->setUpDown (1); triedToMove = true; + mOverencumberedMessageDelay = 0.f; } - if (mAlwaysRunActive) + if (mAlwaysRunActive || isRunning) mPlayer->setRunState(!actionIsActive(A_Run)); else mPlayer->setRunState(actionIsActive(A_Run)); @@ -382,7 +541,7 @@ namespace MWInput { MWWorld::Ptr player = MWBase::Environment::get().getWorld ()->getPlayerPtr(); mOverencumberedMessageDelay -= dt; - if (player.getClass().getEncumbrance(player) >= player.getClass().getCapacity(player)) + if (player.getClass().getEncumbrance(player) > player.getClass().getCapacity(player)) { mPlayer->setAutoMove (false); if (mOverencumberedMessageDelay <= 0) @@ -395,8 +554,7 @@ namespace MWInput if (mControlSwitch["playerviewswitch"]) { - // work around preview mode toggle when pressing Alt+Tab - if (actionIsActive(A_TogglePOV) && !mInputManager->isModifierHeld(SDL_Keymod(KMOD_ALT))) { + if (actionIsActive(A_TogglePOV)) { if (mPreviewPOVDelay <= 0.5 && (mPreviewPOVDelay += dt) > 0.5) { @@ -498,59 +656,26 @@ namespace MWInput void InputManager::keyPressed( const SDL_KeyboardEvent &arg ) { - // Cut, copy & paste - MyGUI::Widget* focus = MyGUI::InputManager::getInstance().getKeyFocusWidget(); - if (focus) - { - MyGUI::EditBox* edit = focus->castType(false); - if (edit && !edit->getEditReadOnly()) - { - if (arg.keysym.sym == SDLK_v && (arg.keysym.mod & SDL_Keymod(KMOD_CTRL))) - { - char* text = SDL_GetClipboardText(); - - if (text) - { - edit->insertText(MyGUI::UString(text), edit->getTextCursor()); - SDL_free(text); - } - } - if (arg.keysym.sym == SDLK_x && (arg.keysym.mod & SDL_Keymod(KMOD_CTRL))) - { - // Discard color codes and other escape characters - std::string text = MyGUI::TextIterator::getOnlyText(edit->getTextSelection()); - if (text.length()) - { - SDL_SetClipboardText(text.c_str()); - edit->deleteTextSelection(); - } - } - } - if (edit && !edit->getEditStatic()) - { - if (arg.keysym.sym == SDLK_c && (arg.keysym.mod & SDL_Keymod(KMOD_CTRL))) - { - // Discard color codes and other escape characters - std::string text = MyGUI::TextIterator::getOnlyText(edit->getTextSelection()); - if (text.length()) - SDL_SetClipboardText(text.c_str()); - } - } - } - + // HACK: to make Morrowind's default keybinding for the console work without printing an extra "^" upon closing + // This assumes that SDL_TextInput events always come *after* the key event + // (which is somewhat reasonable, and hopefully true for all SDL platforms) OIS::KeyCode kc = mInputManager->sdl2OISKeyCode(arg.keysym.sym); + if (mInputBinder->getKeyBinding(mInputBinder->getControl(A_Console), ICS::Control::INCREASE) + == arg.keysym.scancode + && MWBase::Environment::get().getWindowManager()->getMode() == MWGui::GM_Console) + SDL_StopTextInput(); + bool consumed = false; if (kc != OIS::KC_UNASSIGNED) { + consumed = SDL_IsTextInputActive() && + ( !(SDLK_SCANCODE_MASK & arg.keysym.sym) && std::isprint(arg.keysym.sym)); // Little trick to check if key is printable bool guiFocus = MyGUI::InputManager::getInstance().injectKeyPress(MyGUI::KeyCode::Enum(kc), 0); setPlayerControlsEnabled(!guiFocus); } - if (!mControlsDisabled) + if (!mControlsDisabled && !consumed) mInputBinder->keyPressed (arg); - - // Clear MyGUI's clipboard, so it doesn't interfere with our own clipboard implementation. - // We do not use MyGUI's clipboard manager because it doesn't support system clipboard integration with SDL. - MyGUI::ClipboardManager::getInstance().clearClipboardData("Text"); + mJoystickLastUsed = false; } void InputManager::textInput(const SDL_TextInputEvent &arg) @@ -563,6 +688,7 @@ namespace MWInput void InputManager::keyReleased(const SDL_KeyboardEvent &arg ) { + mJoystickLastUsed = false; OIS::KeyCode kc = mInputManager->sdl2OISKeyCode(arg.keysym.sym); setPlayerControlsEnabled(!MyGUI::InputManager::getInstance().injectKeyRelease(MyGUI::KeyCode::Enum(kc))); @@ -571,6 +697,7 @@ namespace MWInput void InputManager::mousePressed( const SDL_MouseButtonEvent &arg, Uint8 id ) { + mJoystickLastUsed = false; bool guiMode = false; if (id == SDL_BUTTON_LEFT || id == SDL_BUTTON_RIGHT) // MyGUI only uses these mouse events @@ -588,13 +715,15 @@ namespace MWInput } setPlayerControlsEnabled(!guiMode); - mInputBinder->mousePressed (arg, id); - + // Don't trigger any mouse bindings while in settings menu, otherwise rebinding controls becomes impossible + if (MWBase::Environment::get().getWindowManager()->getMode() != MWGui::GM_Settings) + mInputBinder->mousePressed (arg, id); } void InputManager::mouseReleased( const SDL_MouseButtonEvent &arg, Uint8 id ) - { + { + mJoystickLastUsed = false; if(mInputBinder->detectingBindingState()) { @@ -614,6 +743,7 @@ namespace MWInput { mInputBinder->mouseMoved (arg); + mJoystickLastUsed = false; resetIdleTime (); if (mGuiCursorEnabled) @@ -633,7 +763,7 @@ namespace MWInput MyGUI::InputManager::getInstance().injectMouseMove( int(mMouseX), int(mMouseY), mMouseWheel); } - if (mMouseLookEnabled) + if (mMouseLookEnabled && !mControlsDisabled) { resetIdleTime(); @@ -652,14 +782,88 @@ namespace MWInput mPlayer->pitch(y); } - if (arg.zrel && mControlSwitch["playerviewswitch"]) //Check to make sure you are allowed to zoomout and there is a change + if (arg.zrel && mControlSwitch["playerviewswitch"] && mControlSwitch["playercontrols"]) //Check to make sure you are allowed to zoomout and there is a change { MWBase::Environment::get().getWorld()->changeVanityModeScale(arg.zrel); - MWBase::Environment::get().getWorld()->setCameraDistance(arg.zrel, true, true); + + if (Settings::Manager::getBool("allow third person zoom", "Input")) + MWBase::Environment::get().getWorld()->setCameraDistance(arg.zrel, true, true); } } } + void InputManager::buttonPressed(int deviceID, const SDL_ControllerButtonEvent &arg ) + { + mJoystickLastUsed = true; + bool guiMode = false; + + if (arg.button == SDL_CONTROLLER_BUTTON_A || arg.button == SDL_CONTROLLER_BUTTON_B) // We'll pretend that A is left click and B is right click + { + guiMode = MWBase::Environment::get().getWindowManager()->isGuiMode(); + if(!mInputBinder->detectingBindingState()) + { + guiMode = MyGUI::InputManager::getInstance().injectMousePress(mMouseX, mMouseY, sdlButtonToMyGUI((arg.button == SDL_CONTROLLER_BUTTON_B) ? SDL_BUTTON_RIGHT : SDL_BUTTON_LEFT)) && guiMode; + if (MyGUI::InputManager::getInstance ().getMouseFocusWidget () != 0) + { + MyGUI::Button* b = MyGUI::InputManager::getInstance ().getMouseFocusWidget ()->castType(false); + if (b && b->getEnabled()) + { + MWBase::Environment::get().getSoundManager ()->playSound ("Menu Click", 1.f, 1.f); + } + } + } + } + + setPlayerControlsEnabled(!guiMode); + + //esc, to leave inital movie screen + OIS::KeyCode kc = mInputManager->sdl2OISKeyCode(SDLK_ESCAPE); + bool guiFocus = MyGUI::InputManager::getInstance().injectKeyPress(MyGUI::KeyCode::Enum(kc), 0); + setPlayerControlsEnabled(!guiFocus); + + if (!mControlsDisabled) + mInputBinder->buttonPressed(deviceID, arg); + } + + void InputManager::buttonReleased(int deviceID, const SDL_ControllerButtonEvent &arg ) + { + mJoystickLastUsed = true; + if(mInputBinder->detectingBindingState()) + mInputBinder->buttonReleased(deviceID, arg); + else if(arg.button == SDL_CONTROLLER_BUTTON_A || arg.button == SDL_CONTROLLER_BUTTON_B) + { + bool guiMode = MWBase::Environment::get().getWindowManager()->isGuiMode(); + guiMode = MyGUI::InputManager::getInstance().injectMouseRelease(mMouseX, mMouseY, sdlButtonToMyGUI((arg.button == SDL_CONTROLLER_BUTTON_B) ? SDL_BUTTON_RIGHT : SDL_BUTTON_LEFT)) && guiMode; + + if(mInputBinder->detectingBindingState()) return; // don't allow same mouseup to bind as initiated bind + + setPlayerControlsEnabled(!guiMode); + mInputBinder->buttonReleased(deviceID, arg); + } + else + mInputBinder->buttonReleased(deviceID, arg); + + //to escape inital movie + OIS::KeyCode kc = mInputManager->sdl2OISKeyCode(SDLK_ESCAPE); + setPlayerControlsEnabled(!MyGUI::InputManager::getInstance().injectKeyRelease(MyGUI::KeyCode::Enum(kc))); + } + + void InputManager::axisMoved(int deviceID, const SDL_ControllerAxisEvent &arg ) + { + mJoystickLastUsed = true; + if (!mControlsDisabled) + mInputBinder->axisMoved(deviceID, arg); + } + + void InputManager::controllerAdded(int deviceID, const SDL_ControllerDeviceEvent &arg) + { + mInputBinder->controllerAdded(deviceID, arg); + } + void InputManager::controllerRemoved(const SDL_ControllerDeviceEvent &arg) + { + mInputBinder->controllerRemoved(arg); + } + void InputManager::windowFocusChange(bool have_focus) { } @@ -689,12 +893,10 @@ namespace MWInput if(!MWBase::Environment::get().getWindowManager()->isGuiMode()) //No open GUIs, open up the MainMenu { MWBase::Environment::get().getWindowManager()->pushGuiMode (MWGui::GM_MainMenu); - MWBase::Environment::get().getSoundManager()->pauseSounds (MWBase::SoundManager::Play_TypeSfx); } else //Close current GUI { MWBase::Environment::get().getWindowManager()->exitCurrentGuiMode(); - MWBase::Environment::get().getSoundManager()->resumeSounds (MWBase::SoundManager::Play_TypeSfx); } } @@ -712,7 +914,7 @@ namespace MWInput if (MWBase::Environment::get().getWindowManager()->isGuiMode()) return; // Not allowed before the magic window is accessible - if (!mControlSwitch["playermagic"]) + if (!mControlSwitch["playermagic"] || !mControlSwitch["playercontrols"]) return; // Not allowed if no spell selected @@ -733,7 +935,7 @@ namespace MWInput if (MWBase::Environment::get().getWindowManager()->isGuiMode()) return; // Not allowed before the inventory window is accessible - if (!mControlSwitch["playerfighting"]) + if (!mControlSwitch["playerfighting"] || !mControlSwitch["playercontrols"]) return; MWMechanics::DrawState_ state = mPlayer->getDrawState(); @@ -745,6 +947,9 @@ namespace MWInput void InputManager::rest() { + if (!mControlSwitch["playercontrols"]) + return; + if (!MWBase::Environment::get().getWindowManager()->getRestEnabled () || MWBase::Environment::get().getWindowManager()->isGuiMode ()) return; @@ -760,12 +965,14 @@ namespace MWInput { mEngine.screenshot(); - std::vector empty; - MWBase::Environment::get().getWindowManager()->messageBox ("Screenshot saved", empty); + MWBase::Environment::get().getWindowManager()->messageBox ("Screenshot saved"); } void InputManager::toggleInventory() { + if (!mControlSwitch["playercontrols"]) + return; + if (MyGUI::InputManager::getInstance ().isModalAny()) return; @@ -802,24 +1009,27 @@ namespace MWInput void InputManager::toggleJournal() { + if (!mControlSwitch["playercontrols"]) + return; if (MyGUI::InputManager::getInstance ().isModalAny()) return; - if((!MWBase::Environment::get().getWindowManager()->isGuiMode() - || MWBase::Environment::get().getWindowManager()->getMode() == MWGui::GM_Dialogue) + if(MWBase::Environment::get().getWindowManager()->getMode() != MWGui::GM_Journal && MWBase::Environment::get().getWindowManager ()->getJournalAllowed()) { MWBase::Environment::get().getSoundManager()->playSound ("book open", 1.0, 1.0); MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Journal); } - else if(MWBase::Environment::get().getWindowManager()->getMode() == MWGui::GM_Journal) + else if(MWBase::Environment::get().getWindowManager()->containsMode(MWGui::GM_Journal)) { - MWBase::Environment::get().getWindowManager()->exitCurrentGuiMode(); + MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_Journal); } } void InputManager::quickKey (int index) { + if (!mControlSwitch["playercontrols"]) + return; MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); if (player.getClass().getNpcStats(player).isWerewolf()) { @@ -899,48 +1109,54 @@ namespace MWInput bool InputManager::actionIsActive (int id) { - return mInputBinder->getChannel (id)->getValue () == 1; + return (mInputBinder->getChannel (id)->getValue ()==1.0); } void InputManager::loadKeyDefaults (bool force) { // using hardcoded key defaults is inevitable, if we want the configuration files to stay valid // across different versions of OpenMW (in the case where another input action is added) - std::map defaultKeyBindings; + std::map defaultKeyBindings; //Gets the Keyvalue from the Scancode; gives the button in the same place reguardless of keyboard format - defaultKeyBindings[A_Activate] = SDL_GetKeyFromScancode(SDL_SCANCODE_SPACE); - defaultKeyBindings[A_MoveBackward] = SDL_GetKeyFromScancode(SDL_SCANCODE_S); - defaultKeyBindings[A_MoveForward] = SDL_GetKeyFromScancode(SDL_SCANCODE_W); - defaultKeyBindings[A_MoveLeft] = SDL_GetKeyFromScancode(SDL_SCANCODE_A); - defaultKeyBindings[A_MoveRight] = SDL_GetKeyFromScancode(SDL_SCANCODE_D); - defaultKeyBindings[A_ToggleWeapon] = SDL_GetKeyFromScancode(SDL_SCANCODE_F); - defaultKeyBindings[A_ToggleSpell] = SDL_GetKeyFromScancode(SDL_SCANCODE_R); - defaultKeyBindings[A_QuickKeysMenu] = SDL_GetKeyFromScancode(SDL_SCANCODE_F1); - defaultKeyBindings[A_Console] = SDL_GetKeyFromScancode(SDL_SCANCODE_F2); - defaultKeyBindings[A_Run] = SDL_GetKeyFromScancode(SDL_SCANCODE_LSHIFT); - defaultKeyBindings[A_Sneak] = SDL_GetKeyFromScancode(SDL_SCANCODE_LCTRL); - defaultKeyBindings[A_AutoMove] = SDL_GetKeyFromScancode(SDL_SCANCODE_Q); - defaultKeyBindings[A_Jump] = SDL_GetKeyFromScancode(SDL_SCANCODE_E); - defaultKeyBindings[A_Journal] = SDL_GetKeyFromScancode(SDL_SCANCODE_J); - defaultKeyBindings[A_Rest] = SDL_GetKeyFromScancode(SDL_SCANCODE_T); - defaultKeyBindings[A_GameMenu] = SDL_GetKeyFromScancode(SDL_SCANCODE_ESCAPE); - defaultKeyBindings[A_TogglePOV] = SDL_GetKeyFromScancode(SDL_SCANCODE_TAB); - defaultKeyBindings[A_QuickKey1] = SDL_GetKeyFromScancode(SDL_SCANCODE_1); - defaultKeyBindings[A_QuickKey2] = SDL_GetKeyFromScancode(SDL_SCANCODE_2); - defaultKeyBindings[A_QuickKey3] = SDL_GetKeyFromScancode(SDL_SCANCODE_3); - defaultKeyBindings[A_QuickKey4] = SDL_GetKeyFromScancode(SDL_SCANCODE_4); - defaultKeyBindings[A_QuickKey5] = SDL_GetKeyFromScancode(SDL_SCANCODE_5); - defaultKeyBindings[A_QuickKey6] = SDL_GetKeyFromScancode(SDL_SCANCODE_6); - defaultKeyBindings[A_QuickKey7] = SDL_GetKeyFromScancode(SDL_SCANCODE_7); - defaultKeyBindings[A_QuickKey8] = SDL_GetKeyFromScancode(SDL_SCANCODE_8); - defaultKeyBindings[A_QuickKey9] = SDL_GetKeyFromScancode(SDL_SCANCODE_9); - defaultKeyBindings[A_QuickKey10] = SDL_GetKeyFromScancode(SDL_SCANCODE_0); - defaultKeyBindings[A_Screenshot] = SDL_GetKeyFromScancode(SDL_SCANCODE_F12); - defaultKeyBindings[A_ToggleHUD] = SDL_GetKeyFromScancode(SDL_SCANCODE_F11); - defaultKeyBindings[A_AlwaysRun] = SDL_GetKeyFromScancode(SDL_SCANCODE_Y); - defaultKeyBindings[A_QuickSave] = SDL_GetKeyFromScancode(SDL_SCANCODE_F5); - defaultKeyBindings[A_QuickLoad] = SDL_GetKeyFromScancode(SDL_SCANCODE_F9); + defaultKeyBindings[A_Activate] = SDL_SCANCODE_SPACE; + defaultKeyBindings[A_MoveBackward] = SDL_SCANCODE_S; + defaultKeyBindings[A_MoveForward] = SDL_SCANCODE_W; + defaultKeyBindings[A_MoveLeft] = SDL_SCANCODE_A; + defaultKeyBindings[A_MoveRight] = SDL_SCANCODE_D; + defaultKeyBindings[A_ToggleWeapon] = SDL_SCANCODE_F; + defaultKeyBindings[A_ToggleSpell] = SDL_SCANCODE_R; + defaultKeyBindings[A_CycleSpellLeft] = SDL_SCANCODE_MINUS; + defaultKeyBindings[A_CycleSpellRight] = SDL_SCANCODE_EQUALS; + defaultKeyBindings[A_CycleWeaponLeft] = SDL_SCANCODE_LEFTBRACKET; + defaultKeyBindings[A_CycleWeaponRight] = SDL_SCANCODE_RIGHTBRACKET; + + defaultKeyBindings[A_QuickKeysMenu] = SDL_SCANCODE_F1; + defaultKeyBindings[A_Console] = SDL_SCANCODE_GRAVE; + defaultKeyBindings[A_Run] = SDL_SCANCODE_LSHIFT; + defaultKeyBindings[A_Sneak] = SDL_SCANCODE_LCTRL; + defaultKeyBindings[A_AutoMove] = SDL_SCANCODE_Q; + defaultKeyBindings[A_Jump] = SDL_SCANCODE_E; + defaultKeyBindings[A_Journal] = SDL_SCANCODE_J; + defaultKeyBindings[A_Rest] = SDL_SCANCODE_T; + defaultKeyBindings[A_GameMenu] = SDL_SCANCODE_ESCAPE; + defaultKeyBindings[A_TogglePOV] = SDL_SCANCODE_TAB; + defaultKeyBindings[A_QuickKey1] = SDL_SCANCODE_1; + defaultKeyBindings[A_QuickKey2] = SDL_SCANCODE_2; + defaultKeyBindings[A_QuickKey3] = SDL_SCANCODE_3; + defaultKeyBindings[A_QuickKey4] = SDL_SCANCODE_4; + defaultKeyBindings[A_QuickKey5] = SDL_SCANCODE_5; + defaultKeyBindings[A_QuickKey6] = SDL_SCANCODE_6; + defaultKeyBindings[A_QuickKey7] = SDL_SCANCODE_7; + defaultKeyBindings[A_QuickKey8] = SDL_SCANCODE_8; + defaultKeyBindings[A_QuickKey9] = SDL_SCANCODE_9; + defaultKeyBindings[A_QuickKey10] = SDL_SCANCODE_0; + defaultKeyBindings[A_Screenshot] = SDL_SCANCODE_F12; + defaultKeyBindings[A_ToggleHUD] = SDL_SCANCODE_F11; + defaultKeyBindings[A_ToggleDebug] = SDL_SCANCODE_F10; + defaultKeyBindings[A_AlwaysRun] = SDL_SCANCODE_CAPSLOCK; + defaultKeyBindings[A_QuickSave] = SDL_SCANCODE_F5; + defaultKeyBindings[A_QuickLoad] = SDL_SCANCODE_F9; std::map defaultMouseButtonBindings; defaultMouseButtonBindings[A_Inventory] = SDL_BUTTON_RIGHT; @@ -962,16 +1178,92 @@ namespace MWInput } if (!controlExists || force || - ( mInputBinder->getKeyBinding (control, ICS::Control::INCREASE) == SDLK_UNKNOWN + ( mInputBinder->getKeyBinding (control, ICS::Control::INCREASE) == SDL_SCANCODE_UNKNOWN && mInputBinder->getMouseButtonBinding (control, ICS::Control::INCREASE) == ICS_MAX_DEVICE_BUTTONS )) { - clearAllBindings (control); + clearAllKeyBindings(control); - if (defaultKeyBindings.find(i) != defaultKeyBindings.end()) - mInputBinder->addKeyBinding(control, static_cast(defaultKeyBindings[i]), ICS::Control::INCREASE); - else if (defaultMouseButtonBindings.find(i) != defaultMouseButtonBindings.end()) + if (defaultKeyBindings.find(i) != defaultKeyBindings.end() + && !mInputBinder->isKeyBound(defaultKeyBindings[i])) + { + control->setInitialValue(0.0f); + mInputBinder->addKeyBinding(control, defaultKeyBindings[i], ICS::Control::INCREASE); + } + else if (defaultMouseButtonBindings.find(i) != defaultMouseButtonBindings.end() + && !mInputBinder->isMouseButtonBound(defaultMouseButtonBindings[i])) + { + control->setInitialValue(0.0f); mInputBinder->addMouseButtonBinding (control, defaultMouseButtonBindings[i], ICS::Control::INCREASE); + } + } + } + } + + void InputManager::loadControllerDefaults(bool force) + { + // using hardcoded key defaults is inevitable, if we want the configuration files to stay valid + // across different versions of OpenMW (in the case where another input action is added) + std::map defaultButtonBindings; + + defaultButtonBindings[A_Activate] = SDL_CONTROLLER_BUTTON_A; + defaultButtonBindings[A_ToggleWeapon] = SDL_CONTROLLER_BUTTON_X; + defaultButtonBindings[A_ToggleSpell] = SDL_CONTROLLER_BUTTON_LEFTSHOULDER; + //defaultButtonBindings[A_QuickButtonsMenu] = SDL_GetButtonFromScancode(SDL_SCANCODE_F1); // Need to implement, should be ToggleSpell(5) AND Wait(9) + defaultButtonBindings[A_Sneak] = SDL_CONTROLLER_BUTTON_RIGHTSTICK; + defaultButtonBindings[A_Jump] = SDL_CONTROLLER_BUTTON_Y; + defaultButtonBindings[A_Journal] = SDL_CONTROLLER_BUTTON_RIGHTSHOULDER; + defaultButtonBindings[A_Rest] = SDL_CONTROLLER_BUTTON_BACK; + defaultButtonBindings[A_TogglePOV] = SDL_CONTROLLER_BUTTON_LEFTSTICK; + defaultButtonBindings[A_Inventory] = SDL_CONTROLLER_BUTTON_B; + defaultButtonBindings[A_GameMenu] = SDL_CONTROLLER_BUTTON_START; + defaultButtonBindings[A_QuickSave] = SDL_CONTROLLER_BUTTON_GUIDE; + defaultButtonBindings[A_QuickKey1] = SDL_CONTROLLER_BUTTON_DPAD_UP; + defaultButtonBindings[A_QuickKey2] = SDL_CONTROLLER_BUTTON_DPAD_LEFT; + defaultButtonBindings[A_QuickKey3] = SDL_CONTROLLER_BUTTON_DPAD_DOWN; + defaultButtonBindings[A_QuickKey4] = SDL_CONTROLLER_BUTTON_DPAD_RIGHT; + + std::map defaultAxisBindings; + defaultAxisBindings[A_MoveForwardBackward] = SDL_CONTROLLER_AXIS_LEFTY; + defaultAxisBindings[A_MoveLeftRight] = SDL_CONTROLLER_AXIS_LEFTX; + defaultAxisBindings[A_LookUpDown] = SDL_CONTROLLER_AXIS_RIGHTY; + defaultAxisBindings[A_LookLeftRight] = SDL_CONTROLLER_AXIS_RIGHTX; + defaultAxisBindings[A_Use] = SDL_CONTROLLER_AXIS_TRIGGERRIGHT; + + for (int i = 0; i < A_Last; i++) + { + ICS::Control* control; + bool controlExists = mInputBinder->getChannel(i)->getControlsCount () != 0; + if (!controlExists) + { + int inital; + if (defaultButtonBindings.find(i) != defaultButtonBindings.end()) + inital = 0.0f; + else inital = 0.5f; + control = new ICS::Control(boost::lexical_cast(i), false, true, inital, ICS::ICS_MAX, ICS::ICS_MAX); + mInputBinder->addControl(control); + control->attachChannel(mInputBinder->getChannel(i), ICS::Channel::DIRECT); + } + else + { + control = mInputBinder->getChannel(i)->getAttachedControls ().front().control; + } + + if (!controlExists || force || ( mInputBinder->getJoystickAxisBinding (control, mFakeDeviceID, ICS::Control::INCREASE) == ICS::InputControlSystem::UNASSIGNED && mInputBinder->getJoystickButtonBinding (control, mFakeDeviceID, ICS::Control::INCREASE) == ICS_MAX_DEVICE_BUTTONS )) + { + clearAllControllerBindings(control); + + if (defaultButtonBindings.find(i) != defaultButtonBindings.end()) + { + control->setInitialValue(0.0f); + mInputBinder->addJoystickButtonBinding(control, mFakeDeviceID, defaultButtonBindings[i], ICS::Control::INCREASE); + } + else if (defaultAxisBindings.find(i) != defaultAxisBindings.end()) + { + control->setValue(0.5f); + control->setInitialValue(0.5f); + mInputBinder->addJoystickAxisBinding(control, mFakeDeviceID, defaultAxisBindings[i], ICS::Control::INCREASE); + } } } } @@ -991,6 +1283,10 @@ namespace MWInput descriptions[A_MoveRight] = "sRight"; descriptions[A_ToggleWeapon] = "sReady_Weapon"; descriptions[A_ToggleSpell] = "sReady_Magic"; + descriptions[A_CycleSpellLeft] = "sPrevSpell"; + descriptions[A_CycleSpellRight] = "sNextSpell"; + descriptions[A_CycleWeaponLeft] = "sPrevWeapon"; + descriptions[A_CycleWeaponRight] = "sNextWeapon"; descriptions[A_Console] = "sConsoleTitle"; descriptions[A_Run] = "sRun"; descriptions[A_Sneak] = "sCrouch_Sneak"; @@ -1021,22 +1317,96 @@ namespace MWInput return "#{" + descriptions[action] + "}"; } - std::string InputManager::getActionBindingName (int action) + std::string InputManager::getActionKeyBindingName (int action) { if (mInputBinder->getChannel (action)->getControlsCount () == 0) return "#{sNone}"; ICS::Control* c = mInputBinder->getChannel (action)->getAttachedControls ().front().control; - if (mInputBinder->getKeyBinding (c, ICS::Control::INCREASE) != SDLK_UNKNOWN) - return mInputBinder->keyCodeToString (mInputBinder->getKeyBinding (c, ICS::Control::INCREASE)); + if (mInputBinder->getKeyBinding (c, ICS::Control::INCREASE) != SDL_SCANCODE_UNKNOWN) + return mInputBinder->scancodeToString (mInputBinder->getKeyBinding (c, ICS::Control::INCREASE)); else if (mInputBinder->getMouseButtonBinding (c, ICS::Control::INCREASE) != ICS_MAX_DEVICE_BUTTONS) return "#{sMouse} " + boost::lexical_cast(mInputBinder->getMouseButtonBinding (c, ICS::Control::INCREASE)); else return "#{sNone}"; } - std::vector InputManager::getActionSorting() + std::string InputManager::getActionControllerBindingName (int action) + { + if (mInputBinder->getChannel (action)->getControlsCount () == 0) + return "#{sNone}"; + + ICS::Control* c = mInputBinder->getChannel (action)->getAttachedControls ().front().control; + + if (mInputBinder->getJoystickAxisBinding (c, mFakeDeviceID, ICS::Control::INCREASE) != ICS::InputControlSystem::UNASSIGNED) + return sdlControllerAxisToString(mInputBinder->getJoystickAxisBinding (c, mFakeDeviceID, ICS::Control::INCREASE)); + else if (mInputBinder->getJoystickButtonBinding (c, mFakeDeviceID, ICS::Control::INCREASE) != ICS_MAX_DEVICE_BUTTONS ) + return sdlControllerButtonToString(mInputBinder->getJoystickButtonBinding (c, mFakeDeviceID, ICS::Control::INCREASE)); + else + return "#{sNone}"; + } + + std::string InputManager::sdlControllerButtonToString(int button) + { + switch(button) + { + case SDL_CONTROLLER_BUTTON_A: + return "A Button"; + case SDL_CONTROLLER_BUTTON_B: + return "B Button"; + case SDL_CONTROLLER_BUTTON_BACK: + return "Back Button"; + case SDL_CONTROLLER_BUTTON_DPAD_DOWN: + return "DPad Down"; + case SDL_CONTROLLER_BUTTON_DPAD_LEFT: + return "DPad Left"; + case SDL_CONTROLLER_BUTTON_DPAD_RIGHT: + return "DPad Right"; + case SDL_CONTROLLER_BUTTON_DPAD_UP: + return "DPad Up"; + case SDL_CONTROLLER_BUTTON_GUIDE: + return "Guide Button"; + case SDL_CONTROLLER_BUTTON_LEFTSHOULDER: + return "Left Shoulder"; + case SDL_CONTROLLER_BUTTON_LEFTSTICK: + return "Left Stick Button"; + case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER: + return "Right Shoulder"; + case SDL_CONTROLLER_BUTTON_RIGHTSTICK: + return "Right Stick Button"; + case SDL_CONTROLLER_BUTTON_START: + return "Start Button"; + case SDL_CONTROLLER_BUTTON_X: + return "X Button"; + case SDL_CONTROLLER_BUTTON_Y: + return "Y Button"; + default: + return "Button " + boost::lexical_cast(button); + } + } + std::string InputManager::sdlControllerAxisToString(int axis) + { + switch(axis) + { + case SDL_CONTROLLER_AXIS_LEFTX: + return "Left Stick X"; + case SDL_CONTROLLER_AXIS_LEFTY: + return "Left Stick Y"; + case SDL_CONTROLLER_AXIS_RIGHTX: + return "Right Stick X"; + case SDL_CONTROLLER_AXIS_RIGHTY: + return "Right Stick Y"; + case SDL_CONTROLLER_AXIS_TRIGGERLEFT: + return "Left Trigger"; + case SDL_CONTROLLER_AXIS_TRIGGERRIGHT: + return "Right Trigger"; + default: + return "Axis " + boost::lexical_cast(axis); + } + } + + std::vector InputManager::getActionKeySorting() { std::vector ret; ret.push_back(A_MoveForward); @@ -1051,6 +1421,10 @@ namespace MWInput ret.push_back(A_Use); ret.push_back(A_ToggleWeapon); ret.push_back(A_ToggleSpell); + ret.push_back(A_CycleSpellLeft); + ret.push_back(A_CycleSpellRight); + ret.push_back(A_CycleWeaponLeft); + ret.push_back(A_CycleWeaponRight); ret.push_back(A_AutoMove); ret.push_back(A_Jump); ret.push_back(A_Inventory); @@ -1074,11 +1448,42 @@ namespace MWInput return ret; } + std::vector InputManager::getActionControllerSorting() + { + std::vector ret; + ret.push_back(A_TogglePOV); + ret.push_back(A_Sneak); + ret.push_back(A_Activate); + ret.push_back(A_Use); + ret.push_back(A_ToggleWeapon); + ret.push_back(A_ToggleSpell); + ret.push_back(A_AutoMove); + ret.push_back(A_Jump); + ret.push_back(A_Inventory); + ret.push_back(A_Journal); + ret.push_back(A_Rest); + ret.push_back(A_QuickSave); + ret.push_back(A_QuickLoad); + ret.push_back(A_Screenshot); + ret.push_back(A_QuickKeysMenu); + ret.push_back(A_QuickKey1); + ret.push_back(A_QuickKey2); + ret.push_back(A_QuickKey3); + ret.push_back(A_QuickKey4); + ret.push_back(A_QuickKey5); + ret.push_back(A_QuickKey6); + ret.push_back(A_QuickKey7); + ret.push_back(A_QuickKey8); + ret.push_back(A_QuickKey9); + ret.push_back(A_QuickKey10); - void InputManager::enableDetectingBindingMode (int action) + return ret; + } + + void InputManager::enableDetectingBindingMode (int action, bool keyboard) { + mDetectingKeyboard = keyboard; ICS::Control* c = mInputBinder->getChannel (action)->getAttachedControls ().front().control; - mInputBinder->enableDetectingBindingState (c, ICS::Control::INCREASE); } @@ -1090,13 +1495,21 @@ namespace MWInput } void InputManager::keyBindingDetected(ICS::InputControlSystem* ICS, ICS::Control* control - , SDL_Keycode key, ICS::Control::ControlChangingDirection direction) + , SDL_Scancode key, ICS::Control::ControlChangingDirection direction) { //Disallow binding escape key - if(key==SDLK_ESCAPE) + if(key==SDL_SCANCODE_ESCAPE) + { + //Stop binding if esc pressed + mInputBinder->cancelDetectingBindingState(); + MWBase::Environment::get().getWindowManager ()->notifyInputActionBound (); + return; + } + if(!mDetectingKeyboard) return; - clearAllBindings(control); + clearAllKeyBindings(control); + control->setInitialValue(0.0f); ICS::DetectingBindingListener::keyBindingDetected (ICS, control, key, direction); MWBase::Environment::get().getWindowManager ()->notifyInputActionBound (); } @@ -1104,59 +1517,69 @@ namespace MWInput void InputManager::mouseButtonBindingDetected(ICS::InputControlSystem* ICS, ICS::Control* control , unsigned int button, ICS::Control::ControlChangingDirection direction) { - clearAllBindings(control); + if(!mDetectingKeyboard) + return; + clearAllKeyBindings(control); + control->setInitialValue(0.0f); ICS::DetectingBindingListener::mouseButtonBindingDetected (ICS, control, button, direction); MWBase::Environment::get().getWindowManager ()->notifyInputActionBound (); } - void InputManager::joystickAxisBindingDetected(ICS::InputControlSystem* ICS, ICS::Control* control - , int deviceId, int axis, ICS::Control::ControlChangingDirection direction) + void InputManager::joystickAxisBindingDetected(ICS::InputControlSystem* ICS, int deviceID, ICS::Control* control + , int axis, ICS::Control::ControlChangingDirection direction) { - clearAllBindings(control); - ICS::DetectingBindingListener::joystickAxisBindingDetected (ICS, control, deviceId, axis, direction); - MWBase::Environment::get().getWindowManager ()->notifyInputActionBound (); - } - - void InputManager::joystickButtonBindingDetected(ICS::InputControlSystem* ICS, ICS::Control* control - , int deviceId, unsigned int button, ICS::Control::ControlChangingDirection direction) - { - clearAllBindings(control); - ICS::DetectingBindingListener::joystickButtonBindingDetected (ICS, control, deviceId, button, direction); - MWBase::Environment::get().getWindowManager ()->notifyInputActionBound (); - } + //only allow binding to the trigers + if(axis != SDL_CONTROLLER_AXIS_TRIGGERLEFT && axis != SDL_CONTROLLER_AXIS_TRIGGERRIGHT) + return; + if(mDetectingKeyboard) + return; - void InputManager::joystickPOVBindingDetected(ICS::InputControlSystem* ICS, ICS::Control* control - , int deviceId, int pov,ICS:: InputControlSystem::POVAxis axis, ICS::Control::ControlChangingDirection direction) - { - clearAllBindings(control); - ICS::DetectingBindingListener::joystickPOVBindingDetected (ICS, control, deviceId, pov, axis, direction); + clearAllControllerBindings(control); + control->setValue(0.5f); //axis bindings must start at 0.5 + control->setInitialValue(0.5f); + ICS::DetectingBindingListener::joystickAxisBindingDetected (ICS, deviceID, control, axis, direction); MWBase::Environment::get().getWindowManager ()->notifyInputActionBound (); } - void InputManager::joystickSliderBindingDetected(ICS::InputControlSystem* ICS, ICS::Control* control - , int deviceId, int slider, ICS::Control::ControlChangingDirection direction) + void InputManager::joystickButtonBindingDetected(ICS::InputControlSystem* ICS, int deviceID, ICS::Control* control + , unsigned int button, ICS::Control::ControlChangingDirection direction) { - clearAllBindings(control); - ICS::DetectingBindingListener::joystickSliderBindingDetected (ICS, control, deviceId, slider, direction); + if(mDetectingKeyboard) + return; + clearAllControllerBindings(control); + control->setInitialValue(0.0f); + ICS::DetectingBindingListener::joystickButtonBindingDetected (ICS, deviceID, control, button, direction); MWBase::Environment::get().getWindowManager ()->notifyInputActionBound (); } - void InputManager::clearAllBindings (ICS::Control* control) + void InputManager::clearAllKeyBindings (ICS::Control* control) { // right now we don't really need multiple bindings for the same action, so remove all others first - if (mInputBinder->getKeyBinding (control, ICS::Control::INCREASE) != SDLK_UNKNOWN) + if (mInputBinder->getKeyBinding (control, ICS::Control::INCREASE) != SDL_SCANCODE_UNKNOWN) mInputBinder->removeKeyBinding (mInputBinder->getKeyBinding (control, ICS::Control::INCREASE)); if (mInputBinder->getMouseButtonBinding (control, ICS::Control::INCREASE) != ICS_MAX_DEVICE_BUTTONS) mInputBinder->removeMouseButtonBinding (mInputBinder->getMouseButtonBinding (control, ICS::Control::INCREASE)); + } - /// \todo add joysticks here once they are added + void InputManager::clearAllControllerBindings (ICS::Control* control) + { + // right now we don't really need multiple bindings for the same action, so remove all others first + if (mInputBinder->getJoystickAxisBinding (control, mFakeDeviceID, ICS::Control::INCREASE) != SDL_SCANCODE_UNKNOWN) + mInputBinder->removeJoystickAxisBinding (mFakeDeviceID, mInputBinder->getJoystickAxisBinding (control, mFakeDeviceID, ICS::Control::INCREASE)); + if (mInputBinder->getJoystickButtonBinding (control, mFakeDeviceID, ICS::Control::INCREASE) != ICS_MAX_DEVICE_BUTTONS) + mInputBinder->removeJoystickButtonBinding (mFakeDeviceID, mInputBinder->getJoystickButtonBinding (control, mFakeDeviceID, ICS::Control::INCREASE)); } - void InputManager::resetToDefaultBindings() + void InputManager::resetToDefaultKeyBindings() { loadKeyDefaults(true); } + void InputManager::resetToDefaultControllerBindings() + { + loadControllerDefaults(true); + } + MyGUI::MouseButton InputManager::sdlButtonToMyGUI(Uint8 button) { //The right button is the second button, according to MyGUI diff --git a/apps/openmw/mwinput/inputmanagerimp.hpp b/apps/openmw/mwinput/inputmanagerimp.hpp index 6bf1ad6b0..c533296ff 100644 --- a/apps/openmw/mwinput/inputmanagerimp.hpp +++ b/apps/openmw/mwinput/inputmanagerimp.hpp @@ -4,6 +4,7 @@ #include "../mwgui/mode.hpp" #include +#include #include "../mwbase/inputmanager.hpp" #include @@ -41,6 +42,11 @@ namespace MyGUI class MouseButton; } +namespace Files +{ + struct ConfigurationManager; +} + #include #include @@ -55,13 +61,15 @@ namespace MWInput public SFO::KeyListener, public SFO::MouseListener, public SFO::WindowListener, + public SFO::ControllerListener, public ICS::ChannelListener, public ICS::DetectingBindingListener { public: InputManager(OEngine::Render::OgreRenderer &_ogre, OMW::Engine& engine, - const std::string& userFile, bool userFileExists, bool grab); + const std::string& userFile, bool userFileExists, + const std::string& controllerBindingsFile, bool grab); virtual ~InputManager(); @@ -82,11 +90,16 @@ namespace MWInput virtual bool getControlSwitch (const std::string& sw); virtual std::string getActionDescription (int action); - virtual std::string getActionBindingName (int action); + virtual std::string getActionKeyBindingName (int action); + virtual std::string getActionControllerBindingName (int action); virtual int getNumActions() { return A_Last; } - virtual std::vector getActionSorting (); - virtual void enableDetectingBindingMode (int action); - virtual void resetToDefaultBindings(); + virtual std::vector getActionKeySorting(); + virtual std::vector getActionControllerSorting(); + virtual void enableDetectingBindingMode (int action, bool keyboard); + virtual void resetToDefaultKeyBindings(); + virtual void resetToDefaultControllerBindings(); + + virtual bool joystickLastUsed() {return mJoystickLastUsed;} public: virtual void keyPressed(const SDL_KeyboardEvent &arg ); @@ -97,6 +110,12 @@ namespace MWInput virtual void mouseReleased( const SDL_MouseButtonEvent &arg, Uint8 id ); virtual void mouseMoved( const SFO::MouseMotionEvent &arg ); + virtual void buttonPressed(int deviceID, const SDL_ControllerButtonEvent &arg); + virtual void buttonReleased(int deviceID, const SDL_ControllerButtonEvent &arg); + virtual void axisMoved(int deviceID, const SDL_ControllerAxisEvent &arg); + virtual void controllerAdded(int deviceID, const SDL_ControllerDeviceEvent &arg); + virtual void controllerRemoved(const SDL_ControllerDeviceEvent &arg); + virtual void windowVisibilityChange( bool visible ); virtual void windowFocusChange( bool have_focus ); virtual void windowResized (int x, int y); @@ -108,26 +127,22 @@ namespace MWInput , ICS::InputControlSystem::NamedAxis axis, ICS::Control::ControlChangingDirection direction); virtual void keyBindingDetected(ICS::InputControlSystem* ICS, ICS::Control* control - , SDL_Keycode key, ICS::Control::ControlChangingDirection direction); + , SDL_Scancode key, ICS::Control::ControlChangingDirection direction); virtual void mouseButtonBindingDetected(ICS::InputControlSystem* ICS, ICS::Control* control , unsigned int button, ICS::Control::ControlChangingDirection direction); - virtual void joystickAxisBindingDetected(ICS::InputControlSystem* ICS, ICS::Control* control - , int deviceId, int axis, ICS::Control::ControlChangingDirection direction); - - virtual void joystickButtonBindingDetected(ICS::InputControlSystem* ICS, ICS::Control* control - , int deviceId, unsigned int button, ICS::Control::ControlChangingDirection direction); - - virtual void joystickPOVBindingDetected(ICS::InputControlSystem* ICS, ICS::Control* control - , int deviceId, int pov,ICS:: InputControlSystem::POVAxis axis, ICS::Control::ControlChangingDirection direction); + virtual void joystickAxisBindingDetected(ICS::InputControlSystem* ICS, int deviceID, ICS::Control* control + , int axis, ICS::Control::ControlChangingDirection direction); - virtual void joystickSliderBindingDetected(ICS::InputControlSystem* ICS, ICS::Control* control - , int deviceId, int slider, ICS::Control::ControlChangingDirection direction); + virtual void joystickButtonBindingDetected(ICS::InputControlSystem* ICS, int deviceID, ICS::Control* control + , unsigned int button, ICS::Control::ControlChangingDirection direction); - void clearAllBindings (ICS::Control* control); + void clearAllKeyBindings (ICS::Control* control); + void clearAllControllerBindings (ICS::Control* control); private: + bool mJoystickLastUsed; OEngine::Render::OgreRenderer &mOgre; MWWorld::Player* mPlayer; OMW::Engine& mEngine; @@ -156,6 +171,8 @@ namespace MWInput bool mMouseLookEnabled; bool mGuiCursorEnabled; + bool mDetectingKeyboard; + float mOverencumberedMessageDelay; float mMouseX; @@ -171,11 +188,16 @@ namespace MWInput void adjustMouseRegion(int width, int height); MyGUI::MouseButton sdlButtonToMyGUI(Uint8 button); + virtual std::string sdlControllerAxisToString(int axis); + virtual std::string sdlControllerButtonToString(int button); + void resetIdleTime(); void updateIdleTime(float dt); void setPlayerControlsEnabled(bool enabled); + void updateCursorMode(); + private: void toggleMainMenu(); void toggleSpell(); @@ -197,6 +219,9 @@ namespace MWInput bool actionIsActive (int id); void loadKeyDefaults(bool force = false); + void loadControllerDefaults(bool force = false); + + int mFakeDeviceID; //As we only support one controller at a time, use a fake deviceID so we don't lose bindings when switching controllers private: enum Actions @@ -259,6 +284,13 @@ namespace MWInput A_ToggleHUD, + A_ToggleDebug, + + A_LookUpDown, //Joystick look + A_LookLeftRight, + A_MoveForwardBackward, + A_MoveLeftRight, + A_Last // Marker for the last item }; }; diff --git a/apps/openmw/mwmechanics/activespells.cpp b/apps/openmw/mwmechanics/activespells.cpp index e64a736c3..eec4f2bd3 100644 --- a/apps/openmw/mwmechanics/activespells.cpp +++ b/apps/openmw/mwmechanics/activespells.cpp @@ -17,18 +17,35 @@ namespace MWMechanics MWWorld::TimeStamp now = MWBase::Environment::get().getWorld()->getTimeStamp(); - // Erase no longer active spells + // Erase no longer active spells and effects if (mLastUpdate!=now) { TContainer::iterator iter (mSpells.begin()); while (iter!=mSpells.end()) + { if (!timeToExpire (iter)) { mSpells.erase (iter++); rebuild = true; } else + { + std::vector& effects = iter->second.mEffects; + for (std::vector::iterator effectIt = effects.begin(); effectIt != effects.end();) + { + MWWorld::TimeStamp start = iter->second.mTimeStamp; + MWWorld::TimeStamp end = start + static_cast(effectIt->mDuration)*MWBase::Environment::get().getWorld()->getTimeScaleFactor()/(60*60); + if (end <= now) + { + effectIt = effects.erase(effectIt); + rebuild = true; + } + else + ++effectIt; + } ++iter; + } + } mLastUpdate = now; } @@ -135,12 +152,7 @@ namespace MWMechanics void ActiveSpells::addSpell(const std::string &id, bool stack, std::vector effects, const std::string &displayName, int casterActorId) { - bool exists = false; - for (TContainer::const_iterator it = begin(); it != end(); ++it) - { - if (id == it->first) - exists = true; - } + TContainer::iterator it(mSpells.find(id)); ActiveSpellParams params; params.mTimeStamp = MWBase::Environment::get().getWorld()->getTimeStamp(); @@ -148,14 +160,44 @@ namespace MWMechanics params.mDisplayName = displayName; params.mCasterActorId = casterActorId; - if (!exists || stack) - mSpells.insert (std::make_pair(id, params)); + if (it == end() || stack) + { + mSpells.insert(std::make_pair(id, params)); + } else - mSpells.find(id)->second = params; + { + // addSpell() is called with effects for a range. + // but a spell may have effects with different ranges (e.g. Touch & Target) + // so, if we see new effects for same spell assume additional + // spell effects and add to existing effects of spell + mergeEffects(params.mEffects, it->second.mEffects); + it->second = params; + } mSpellsChanged = true; } + void ActiveSpells::mergeEffects(std::vector& addTo, const std::vector& from) + { + for (std::vector::const_iterator effect(from.begin()); effect != from.end(); ++effect) + { + // if effect is not in addTo, add it + bool missing = true; + for (std::vector::const_iterator iter(addTo.begin()); iter != addTo.end(); ++iter) + { + if (effect->mEffectId == iter->mEffectId) + { + missing = false; + break; + } + } + if (missing) + { + addTo.push_back(*effect); + } + } + } + void ActiveSpells::removeEffects(const std::string &id) { mSpells.erase(Misc::StringUtils::lowerCase(id)); @@ -178,7 +220,7 @@ namespace MWMechanics float magnitude = effectIt->mMagnitude; if (magnitude) - visitor.visit(MWMechanics::EffectKey(effectIt->mEffectId, effectIt->mArg), name, it->second.mCasterActorId, magnitude, remainingTime); + visitor.visit(MWMechanics::EffectKey(effectIt->mEffectId, effectIt->mArg), name, it->first, it->second.mCasterActorId, magnitude, remainingTime, effectIt->mDuration); } } } @@ -212,6 +254,22 @@ namespace MWMechanics mSpellsChanged = true; } + void ActiveSpells::purgeEffect(short effectId, const std::string& sourceId) + { + for (TContainer::iterator it = mSpells.begin(); it != mSpells.end(); ++it) + { + for (std::vector::iterator effectIt = it->second.mEffects.begin(); + effectIt != it->second.mEffects.end();) + { + if (effectIt->mEffectId == effectId && it->first == sourceId) + effectIt = it->second.mEffects.erase(effectIt); + else + ++effectIt; + } + } + mSpellsChanged = true; + } + void ActiveSpells::purge(int casterActorId) { for (TContainer::iterator it = mSpells.begin(); it != mSpells.end(); ++it) diff --git a/apps/openmw/mwmechanics/activespells.hpp b/apps/openmw/mwmechanics/activespells.hpp index 9c1a5e613..2a4d75d40 100644 --- a/apps/openmw/mwmechanics/activespells.hpp +++ b/apps/openmw/mwmechanics/activespells.hpp @@ -55,6 +55,9 @@ namespace MWMechanics void rebuildEffects() const; + /// Add any effects that are in "from" and not in "addTo" to "addTo" + void mergeEffects(std::vector& addTo, const std::vector& from); + double timeToExpire (const TIterator& iterator) const; ///< Returns time (in in-game hours) until the spell pointed to by \a iterator /// expires. @@ -82,6 +85,9 @@ namespace MWMechanics /// Remove all active effects with this effect id void purgeEffect (short effectId); + /// Remove all active effects with this effect id and source id + void purgeEffect (short effectId, const std::string& sourceId); + /// Remove all active effects, if roll succeeds (for each effect) void purgeAll (float chance); diff --git a/apps/openmw/mwmechanics/actor.cpp b/apps/openmw/mwmechanics/actor.cpp new file mode 100644 index 000000000..675bd160a --- /dev/null +++ b/apps/openmw/mwmechanics/actor.cpp @@ -0,0 +1,28 @@ +#include "actor.hpp" + +#include "character.hpp" + +namespace MWMechanics +{ + + Actor::Actor(const MWWorld::Ptr &ptr, MWRender::Animation *animation) + { + mCharacterController.reset(new CharacterController(ptr, animation)); + } + + void Actor::updatePtr(const MWWorld::Ptr &newPtr) + { + mCharacterController->updatePtr(newPtr); + } + + CharacterController* Actor::getCharacterController() + { + return mCharacterController.get(); + } + + AiState& Actor::getAiState() + { + return mAiState; + } + +} diff --git a/apps/openmw/mwmechanics/actor.hpp b/apps/openmw/mwmechanics/actor.hpp new file mode 100644 index 000000000..846af1467 --- /dev/null +++ b/apps/openmw/mwmechanics/actor.hpp @@ -0,0 +1,42 @@ +#ifndef OPENMW_MECHANICS_ACTOR_H +#define OPENMW_MECHANICS_ACTOR_H + +#include + +#include "aistate.hpp" + +namespace MWRender +{ + class Animation; +} +namespace MWWorld +{ + class Ptr; +} + +namespace MWMechanics +{ + class CharacterController; + + /// @brief Holds temporary state for an actor that will be discarded when the actor leaves the scene. + class Actor + { + public: + Actor(const MWWorld::Ptr& ptr, MWRender::Animation* animation); + + /// Notify this actor of its new base object Ptr, use when the object changed cells + void updatePtr(const MWWorld::Ptr& newPtr); + + CharacterController* getCharacterController(); + + AiState& getAiState(); + + private: + std::auto_ptr mCharacterController; + + AiState mAiState; + }; + +} + +#endif diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index 8f1209827..aa2cc8615 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -4,6 +4,7 @@ #include #include +#include #include @@ -25,6 +26,7 @@ #include "npcstats.hpp" #include "creaturestats.hpp" #include "movement.hpp" +#include "character.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" @@ -33,9 +35,19 @@ #include "aifollow.hpp" #include "aipursue.hpp" +#include "actor.hpp" +#include "summoning.hpp" +#include "combat.hpp" + namespace { +bool isConscious(const MWWorld::Ptr& ptr) +{ + const MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); + return !stats.isDead() && !stats.getKnockedDown(); +} + void adjustBoundItem (const std::string& item, bool bound, const MWWorld::Ptr& actor) { if (bound) @@ -69,6 +81,8 @@ bool disintegrateSlot (MWWorld::Ptr ptr, int slot, float disintegrate) if (charge == 0) return false; + // FIXME: charge should be a float, not int so that damage < 1 per frame can be applied. + // This was also a bug in the original engine. charge -= std::min(disintegrate, static_cast(charge)); @@ -89,12 +103,64 @@ bool disintegrateSlot (MWWorld::Ptr ptr, int slot, float disintegrate) return false; } +class CheckActorCommanded : public MWMechanics::EffectSourceVisitor +{ + MWWorld::Ptr mActor; +public: + bool mCommanded; + CheckActorCommanded(MWWorld::Ptr actor) + : mActor(actor) + , mCommanded(false){} + + virtual void visit (MWMechanics::EffectKey key, + const std::string& sourceName, const std::string& sourceId, int casterActorId, + float magnitude, float remainingTime = -1, float totalTime = -1) + { + MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); + if ( ((key.mId == ESM::MagicEffect::CommandHumanoid && mActor.getClass().isNpc()) + || (key.mId == ESM::MagicEffect::CommandCreature && mActor.getTypeName() == typeid(ESM::Creature).name())) + && casterActorId == player.getClass().getCreatureStats(player).getActorId() + && magnitude >= mActor.getClass().getCreatureStats(mActor).getLevel()) + mCommanded = true; + } +}; + +void adjustCommandedActor (const MWWorld::Ptr& actor) +{ + CheckActorCommanded check(actor); + MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor); + stats.getActiveSpells().visitEffectSources(check); + + bool hasCommandPackage = false; + + std::list::const_iterator it; + for (it = stats.getAiSequence().begin(); it != stats.getAiSequence().end(); ++it) + { + if ((*it)->getTypeId() == MWMechanics::AiPackage::TypeIdFollow && + static_cast(*it)->isCommanded()) + { + hasCommandPackage = true; + break; + } + } + + if (check.mCommanded && !hasCommandPackage) + { + MWMechanics::AiFollow package("player", true); + stats.getAiSequence().stack(package, actor); + } + else if (!check.mCommanded && hasCommandPackage) + { + stats.getAiSequence().erase(it); + } +} + void getRestorationPerHourOfSleep (const MWWorld::Ptr& ptr, float& health, float& magicka) { MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats (ptr); const MWWorld::Store& settings = MWBase::Environment::get().getWorld()->getStore().get(); - bool stunted = stats.getMagicEffects ().get(ESM::MagicEffect::StuntedMagicka).mMagnitude > 0; + bool stunted = stats.getMagicEffects ().get(ESM::MagicEffect::StuntedMagicka).getMagnitude() > 0; int endurance = stats.getAttribute (ESM::Attribute::Endurance).getModified (); health = 0.1 * endurance; @@ -121,8 +187,8 @@ namespace MWMechanics : mCreature(trappedCreature) {} virtual void visit (MWMechanics::EffectKey key, - const std::string& sourceName, int casterActorId, - float magnitude, float remainingTime = -1) + const std::string& sourceName, const std::string& sourceId, int casterActorId, + float magnitude, float remainingTime = -1, float totalTime = -1) { if (key.mId != ESM::MagicEffect::Soultrap) return; @@ -144,7 +210,7 @@ namespace MWMechanics // Use the smallest soulgem that is large enough to hold the soul MWWorld::ContainerStore& container = caster.getClass().getContainerStore(caster); MWWorld::ContainerStoreIterator gem = container.end(); - float gemCapacity = std::numeric_limits().max(); + float gemCapacity = std::numeric_limits::max(); std::string soulgemFilter = "misc_soulgem"; // no other way to check for soulgems? :/ for (MWWorld::ContainerStoreIterator it = container.begin(MWWorld::ContainerStore::Type_Miscellaneous); it != container.end(); ++it) @@ -172,6 +238,15 @@ namespace MWMechanics if (caster.getRefData().getHandle() == "player") MWBase::Environment::get().getWindowManager()->messageBox("#{sSoultrapSuccess}"); + + const ESM::Static* fx = MWBase::Environment::get().getWorld()->getStore().get() + .search("VFX_Soul_Trap"); + if (fx) + MWBase::Environment::get().getWorld()->spawnEffect("meshes\\" + fx->mModel, + "", Ogre::Vector3(mCreature.getRefData().getPosition().pos)); + + MWBase::Environment::get().getSoundManager()->playSound3D(mCreature, "conjuration hit", 1.f, 1.f, + MWBase::SoundManager::Play_TypeSfx, MWBase::SoundManager::Play_NoTrack); } }; @@ -187,55 +262,131 @@ namespace MWMechanics calculateRestoration(ptr, duration); } + void Actors::updateHeadTracking(const MWWorld::Ptr& actor, const MWWorld::Ptr& targetActor, + MWWorld::Ptr& headTrackTarget, float& sqrHeadTrackDistance) + { + static const float fMaxHeadTrackDistance = MWBase::Environment::get().getWorld()->getStore().get() + .find("fMaxHeadTrackDistance")->getFloat(); + static const float fInteriorHeadTrackMult = MWBase::Environment::get().getWorld()->getStore().get() + .find("fInteriorHeadTrackMult")->getFloat(); + float maxDistance = fMaxHeadTrackDistance; + const ESM::Cell* currentCell = actor.getCell()->getCell(); + if (!currentCell->isExterior() && !(currentCell->mData.mFlags & ESM::Cell::QuasiEx)) + maxDistance *= fInteriorHeadTrackMult; + + const ESM::Position& actor1Pos = actor.getRefData().getPosition(); + const ESM::Position& actor2Pos = targetActor.getRefData().getPosition(); + float sqrDist = Ogre::Vector3(actor1Pos.pos).squaredDistance(Ogre::Vector3(actor2Pos.pos)); + + if (sqrDist > maxDistance*maxDistance) + return; + + if (targetActor.getClass().getCreatureStats(targetActor).isDead()) + return; + + // stop tracking when target is behind the actor + Ogre::Vector3 actorDirection (actor.getRefData().getBaseNode()->getOrientation().yAxis()); + Ogre::Vector3 targetDirection (Ogre::Vector3(actor2Pos.pos) - Ogre::Vector3(actor1Pos.pos)); + actorDirection.z = 0; + targetDirection.z = 0; + if (actorDirection.angleBetween(targetDirection) < Ogre::Degree(90) + && sqrDist <= sqrHeadTrackDistance + && MWBase::Environment::get().getWorld()->getLOS(actor, targetActor) // check LOS and awareness last as it's the most expensive function + && MWBase::Environment::get().getMechanicsManager()->awarenessCheck(targetActor, actor)) + { + sqrHeadTrackDistance = sqrDist; + headTrackTarget = targetActor; + } + } + void Actors::engageCombat (const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2, bool againstPlayer) { CreatureStats& creatureStats = actor1.getClass().getCreatureStats(actor1); - - if (againstPlayer && creatureStats.isHostile()) return; // already fighting against player + + if (actor2.getClass().getCreatureStats(actor2).isDead() + || actor1.getClass().getCreatureStats(actor1).isDead()) + return; + + const ESM::Position& actor1Pos = actor1.getRefData().getPosition(); + const ESM::Position& actor2Pos = actor2.getRefData().getPosition(); + float sqrDist = Ogre::Vector3(actor1Pos.pos).squaredDistance(Ogre::Vector3(actor2Pos.pos)); + if (sqrDist > 7168*7168) + return; // pure water creatures won't try to fight with the target on the ground // except that creature is already hostile - if ((againstPlayer || !creatureStats.isHostile()) - && ((actor1.getClass().canSwim(actor1) && !actor1.getClass().canWalk(actor1) // pure water creature - && !MWBase::Environment::get().getWorld()->isSwimming(actor2)) - || (!actor1.getClass().canSwim(actor1) && MWBase::Environment::get().getWorld()->isSwimming(actor2)))) // creature can't swim to target + if ((againstPlayer || !creatureStats.getAiSequence().isInCombat()) + && !MWMechanics::isEnvironmentCompatible(actor1, actor2)) // creature can't swim to target + { return; + } bool aggressive; - if (againstPlayer) + if (againstPlayer) + { + // followers with high fight should not engage in combat with the player (e.g. bm_bear_black_summon) + const std::list& followers = getActorsFollowing(actor2); + if (std::find(followers.begin(), followers.end(), actor1) != followers.end()) + return; + aggressive = MWBase::Environment::get().getMechanicsManager()->isAggressive(actor1, actor2); + } else { aggressive = false; - // if one of actors is creature then we should make a decision to start combat or not - // NOTE: function doesn't take into account combat between 2 creatures - if (!actor1.getClass().isNpc()) + + // Make guards fight aggressive creatures + if (!actor1.getClass().isNpc() && actor2.getClass().isClass(actor2, "Guard")) { - // if creature is hostile then it is necessarily to start combat - if (creatureStats.isHostile()) aggressive = true; - else aggressive = MWBase::Environment::get().getMechanicsManager()->isAggressive(actor1, actor2); + if (creatureStats.getAiSequence().isInCombat() && MWBase::Environment::get().getMechanicsManager()->isAggressive(actor1, actor2)) + aggressive = true; } } - if(aggressive) + // start combat if target actor is in combat with one of our followers + const std::list& followers = getActorsFollowing(actor1); + const CreatureStats& creatureStats2 = actor2.getClass().getCreatureStats(actor2); + for (std::list::const_iterator it = followers.begin(); it != followers.end(); ++it) { - const ESM::Position& actor1Pos = actor2.getRefData().getPosition(); - const ESM::Position& actor2Pos = actor2.getRefData().getPosition(); - float d = Ogre::Vector3(actor1Pos.pos).distance(Ogre::Vector3(actor2Pos.pos)); - if (againstPlayer || actor2.getClass().getCreatureStats(actor2).getAiSequence().canAddTarget(actor2Pos, d)) + // need to check both ways since player doesn't use AI packages + if ((creatureStats2.getAiSequence().isInCombat(*it) + || it->getClass().getCreatureStats(*it).getAiSequence().isInCombat(actor2)) + && !creatureStats.getAiSequence().isInCombat(*it)) + aggressive = true; + } + + // start combat if target actor is in combat with someone we are following + for (std::list::const_iterator it = creatureStats.getAiSequence().begin(); it != creatureStats.getAiSequence().end(); ++it) + { + if ((*it)->getTypeId() == MWMechanics::AiPackage::TypeIdFollow) { - bool LOS = MWBase::Environment::get().getWorld()->getLOS(actor1, actor2); + MWWorld::Ptr followTarget = static_cast(*it)->getTarget(); + if (followTarget.isEmpty()) + continue; + + if (creatureStats.getAiSequence().isInCombat(followTarget)) + continue; + + // need to check both ways since player doesn't use AI packages + if (creatureStats2.getAiSequence().isInCombat(followTarget) + || followTarget.getClass().getCreatureStats(followTarget).getAiSequence().isInCombat(actor2)) + aggressive = true; + } + } + + if(aggressive) + { + bool LOS = MWBase::Environment::get().getWorld()->getLOS(actor1, actor2); - if (againstPlayer) LOS &= MWBase::Environment::get().getMechanicsManager()->awarenessCheck(actor2, actor1); + if (againstPlayer) LOS &= MWBase::Environment::get().getMechanicsManager()->awarenessCheck(actor2, actor1); - if (LOS) + if (LOS) + { + MWBase::Environment::get().getMechanicsManager()->startCombat(actor1, actor2); + if (!againstPlayer) // start combat between each other { - MWBase::Environment::get().getMechanicsManager()->startCombat(actor1, actor2); - if (!againstPlayer) // start combat between each other - { - MWBase::Environment::get().getMechanicsManager()->startCombat(actor2, actor1); - } + MWBase::Environment::get().getMechanicsManager()->startCombat(actor2, actor1); } } } @@ -244,13 +395,15 @@ namespace MWMechanics void Actors::updateNpc (const MWWorld::Ptr& ptr, float duration) { updateDrowning(ptr, duration); - calculateNpcStatModifiers(ptr); + calculateNpcStatModifiers(ptr, duration); updateEquippedLight(ptr, duration); } void Actors::adjustMagicEffects (const MWWorld::Ptr& creature) { CreatureStats& creatureStats = creature.getClass().getCreatureStats (creature); + if (creatureStats.isDead()) + return; MagicEffects now = creatureStats.getSpells().getMagicEffects(); @@ -262,35 +415,28 @@ namespace MWMechanics now += creatureStats.getActiveSpells().getMagicEffects(); - //MagicEffects diff = MagicEffects::diff (creatureStats.getMagicEffects(), now); - - creatureStats.setMagicEffects(now); - - // TODO apply diff to other stats + creatureStats.modifyMagicEffects(now); } void Actors::calculateDynamicStats (const MWWorld::Ptr& ptr) { CreatureStats& creatureStats = ptr.getClass().getCreatureStats (ptr); - int strength = creatureStats.getAttribute(ESM::Attribute::Strength).getBase(); - int intelligence = creatureStats.getAttribute(ESM::Attribute::Intelligence).getBase(); - int willpower = creatureStats.getAttribute(ESM::Attribute::Willpower).getBase(); - int agility = creatureStats.getAttribute(ESM::Attribute::Agility).getBase(); - int endurance = creatureStats.getAttribute(ESM::Attribute::Endurance).getBase(); + int intelligence = creatureStats.getAttribute(ESM::Attribute::Intelligence).getModified(); - double magickaFactor = - creatureStats.getMagicEffects().get (EffectKey (ESM::MagicEffect::FortifyMaximumMagicka)).mMagnitude * 0.1 + 0.5; + float base = 1.f; + if (ptr.getCellRef().getRefId() == "player") + base = MWBase::Environment::get().getWorld()->getStore().get().find("fPCbaseMagickaMult")->getFloat(); + else + base = MWBase::Environment::get().getWorld()->getStore().get().find("fNPCbaseMagickaMult")->getFloat(); + + double magickaFactor = base + + creatureStats.getMagicEffects().get (EffectKey (ESM::MagicEffect::FortifyMaximumMagicka)).getMagnitude() * 0.1; DynamicStat magicka = creatureStats.getMagicka(); - float diff = (static_cast(intelligence + magickaFactor*intelligence)) - magicka.getBase(); + float diff = (static_cast(magickaFactor*intelligence)) - magicka.getBase(); magicka.modify(diff); creatureStats.setMagicka(magicka); - - DynamicStat fatigue = creatureStats.getFatigue(); - diff = (strength+willpower+agility+endurance) - fatigue.getBase(); - fatigue.modify(diff); - creatureStats.setFatigue(fatigue); } void Actors::restoreDynamicStats (const MWWorld::Ptr& ptr, bool sleep) @@ -317,9 +463,7 @@ namespace MWMechanics int endurance = stats.getAttribute (ESM::Attribute::Endurance).getModified (); - float capacity = ptr.getClass().getCapacity(ptr); - float encumbrance = ptr.getClass().getEncumbrance(ptr); - float normalizedEncumbrance = (capacity == 0 ? 1 : encumbrance/capacity); + float normalizedEncumbrance = ptr.getClass().getNormalizedEncumbrance(ptr); if (normalizedEncumbrance > 1) normalizedEncumbrance = 1; @@ -342,13 +486,13 @@ namespace MWMechanics return; MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats (ptr); - const MWWorld::Store& settings = MWBase::Environment::get().getWorld()->getStore().get(); int endurance = stats.getAttribute (ESM::Attribute::Endurance).getModified (); // restore fatigue - float fFatigueReturnBase = settings.find("fFatigueReturnBase")->getFloat (); - float fFatigueReturnMult = settings.find("fFatigueReturnMult")->getFloat (); + const MWWorld::Store& settings = MWBase::Environment::get().getWorld()->getStore().get(); + static const float fFatigueReturnBase = settings.find("fFatigueReturnBase")->getFloat (); + static const float fFatigueReturnMult = settings.find("fFatigueReturnMult")->getFloat (); float x = fFatigueReturnBase + fFatigueReturnMult * endurance; @@ -364,28 +508,51 @@ namespace MWMechanics bool wasDead = creatureStats.isDead(); + // FIXME: effect ticks should go into separate functions so they can be used with either + // magnitude (instant effect) or magnitude*duration + // attributes for(int i = 0;i < ESM::Attribute::Length;++i) { AttributeValue stat = creatureStats.getAttribute(i); - stat.setModifier(effects.get(EffectKey(ESM::MagicEffect::FortifyAttribute, i)).mMagnitude - - effects.get(EffectKey(ESM::MagicEffect::DrainAttribute, i)).mMagnitude - - effects.get(EffectKey(ESM::MagicEffect::AbsorbAttribute, i)).mMagnitude); + stat.setModifiers(effects.get(EffectKey(ESM::MagicEffect::FortifyAttribute, i)).getMagnitude(), + effects.get(EffectKey(ESM::MagicEffect::DrainAttribute, i)).getMagnitude(), + effects.get(EffectKey(ESM::MagicEffect::AbsorbAttribute, i)).getMagnitude()); + + stat.damage(effects.get(EffectKey(ESM::MagicEffect::DamageAttribute, i)).getMagnitude() * duration * 1.5); + stat.restore(effects.get(EffectKey(ESM::MagicEffect::RestoreAttribute, i)).getMagnitude() * duration * 1.5); creatureStats.setAttribute(i, stat); } + { + Spells & spells = creatureStats.getSpells(); + for (Spells::TIterator it = spells.begin(); it != spells.end(); ++it) + { + if (spells.getCorprusSpells().find(it->first) != spells.getCorprusSpells().end()) + { + if (MWBase::Environment::get().getWorld()->getTimeStamp() >= spells.getCorprusSpells().at(it->first).mNextWorsening) + { + spells.worsenCorprus(it->first); + + if (ptr.getRefData().getHandle() == "player") + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicCorprusWorsens}"); + } + } + } + } + // dynamic stats for(int i = 0;i < 3;++i) { DynamicStat stat = creatureStats.getDynamic(i); - stat.setModifier(effects.get(ESM::MagicEffect::FortifyHealth+i).mMagnitude - - effects.get(ESM::MagicEffect::DrainHealth+i).mMagnitude); + stat.setModifier(effects.get(ESM::MagicEffect::FortifyHealth+i).getMagnitude() - + effects.get(ESM::MagicEffect::DrainHealth+i).getMagnitude()); - float currentDiff = creatureStats.getMagicEffects().get(ESM::MagicEffect::RestoreHealth+i).mMagnitude - - creatureStats.getMagicEffects().get(ESM::MagicEffect::DamageHealth+i).mMagnitude - - creatureStats.getMagicEffects().get(ESM::MagicEffect::AbsorbHealth+i).mMagnitude; + float currentDiff = creatureStats.getMagicEffects().get(ESM::MagicEffect::RestoreHealth+i).getMagnitude() + - creatureStats.getMagicEffects().get(ESM::MagicEffect::DamageHealth+i).getMagnitude() + - creatureStats.getMagicEffects().get(ESM::MagicEffect::AbsorbHealth+i).getMagnitude(); stat.setCurrent(stat.getCurrent() + currentDiff * duration, i == 2); creatureStats.setDynamic(i, stat); @@ -399,27 +566,27 @@ namespace MWMechanics if (!creature || ptr.get()->mBase->mData.mType == ESM::Creature::Creatures) { Stat stat = creatureStats.getAiSetting(CreatureStats::AI_Fight); - stat.setModifier(creatureStats.getMagicEffects().get(ESM::MagicEffect::FrenzyHumanoid+creature).mMagnitude - - creatureStats.getMagicEffects().get(ESM::MagicEffect::CalmHumanoid+creature).mMagnitude); + stat.setModifier(creatureStats.getMagicEffects().get(ESM::MagicEffect::FrenzyHumanoid+creature).getMagnitude() + - creatureStats.getMagicEffects().get(ESM::MagicEffect::CalmHumanoid+creature).getMagnitude()); creatureStats.setAiSetting(CreatureStats::AI_Fight, stat); stat = creatureStats.getAiSetting(CreatureStats::AI_Flee); - stat.setModifier(creatureStats.getMagicEffects().get(ESM::MagicEffect::DemoralizeHumanoid+creature).mMagnitude - - creatureStats.getMagicEffects().get(ESM::MagicEffect::RallyHumanoid+creature).mMagnitude); + stat.setModifier(creatureStats.getMagicEffects().get(ESM::MagicEffect::DemoralizeHumanoid+creature).getMagnitude() + - creatureStats.getMagicEffects().get(ESM::MagicEffect::RallyHumanoid+creature).getMagnitude()); creatureStats.setAiSetting(CreatureStats::AI_Flee, stat); } if (creature && ptr.get()->mBase->mData.mType == ESM::Creature::Undead) { Stat stat = creatureStats.getAiSetting(CreatureStats::AI_Flee); - stat.setModifier(creatureStats.getMagicEffects().get(ESM::MagicEffect::TurnUndead).mMagnitude); + stat.setModifier(creatureStats.getMagicEffects().get(ESM::MagicEffect::TurnUndead).getMagnitude()); creatureStats.setAiSetting(CreatureStats::AI_Flee, stat); } // Apply disintegration (reduces item health) - float disintegrateWeapon = effects.get(ESM::MagicEffect::DisintegrateWeapon).mMagnitude; + float disintegrateWeapon = effects.get(ESM::MagicEffect::DisintegrateWeapon).getMagnitude(); if (disintegrateWeapon > 0) disintegrateSlot(ptr, MWWorld::InventoryStore::Slot_CarriedRight, disintegrateWeapon*duration); - float disintegrateArmor = effects.get(ESM::MagicEffect::DisintegrateArmor).mMagnitude; + float disintegrateArmor = effects.get(ESM::MagicEffect::DisintegrateArmor).getMagnitude(); if (disintegrateArmor > 0) { // According to UESP @@ -442,6 +609,12 @@ namespace MWMechanics } } + bool receivedMagicDamage = false; + + if (creatureStats.getMagicEffects().get(ESM::MagicEffect::DamageHealth).getMagnitude() > 0.0f + || creatureStats.getMagicEffects().get(ESM::MagicEffect::AbsorbHealth).getMagnitude() > 0.0f) + receivedMagicDamage = true; + // Apply damage ticks int damageEffects[] = { ESM::MagicEffect::FireDamage, ESM::MagicEffect::ShockDamage, ESM::MagicEffect::FrostDamage, ESM::MagicEffect::Poison, @@ -451,7 +624,7 @@ namespace MWMechanics DynamicStat health = creatureStats.getHealth(); for (unsigned int i=0; i 1) damageScale *= fMagicSunBlockedMult; health.setCurrent(health.getCurrent() - magnitude * duration * damageScale); + + if (magnitude * damageScale > 0.0f) + receivedMagicDamage = true; } else + { health.setCurrent(health.getCurrent() - magnitude * duration); + if (magnitude > 0.0f) + receivedMagicDamage = true; + } } + + if (receivedMagicDamage && ptr.getRefData().getHandle() == "player") + MWBase::Environment::get().getWindowManager()->activateHitOverlay(false); + creatureStats.setHealth(health); if (!wasDead && creatureStats.isDead()) @@ -522,7 +706,22 @@ namespace MWMechanics // TODO: dirty flag for magic effects to avoid some unnecessary work below? + // any value of calm > 0 will stop the actor from fighting + if ((creatureStats.getMagicEffects().get(ESM::MagicEffect::CalmHumanoid).getMagnitude() > 0 && ptr.getClass().isNpc()) + || (creatureStats.getMagicEffects().get(ESM::MagicEffect::CalmCreature).getMagnitude() > 0 && !ptr.getClass().isNpc())) + { + for (std::list::const_iterator it = creatureStats.getAiSequence().begin(); it != creatureStats.getAiSequence().end(); ) + { + if ((*it)->getTypeId() == AiPackage::TypeIdCombat) + it = creatureStats.getAiSequence().erase(it); + else + ++it; + } + } + // Update bound effects + // Note: in vanilla MW multiple bound items of the same type can be created by different spells. + // As these extra copies are kinda useless this may or may not be important. static std::map boundItemsMap; if (boundItemsMap.empty()) { @@ -542,7 +741,7 @@ namespace MWMechanics for (std::map::iterator it = boundItemsMap.begin(); it != boundItemsMap.end(); ++it) { bool found = creatureStats.mBoundItems.find(it->first) != creatureStats.mBoundItems.end(); - int magnitude = creatureStats.getMagicEffects().get(it->first).mMagnitude; + int magnitude = creatureStats.getMagicEffects().get(it->first).getMagnitude(); if (found != (magnitude > 0)) { std::string itemGmst = it->second; @@ -567,137 +766,14 @@ namespace MWMechanics } } - // Update summon effects - static std::map summonMap; - if (summonMap.empty()) - { - summonMap[ESM::MagicEffect::SummonAncestralGhost] = "sMagicAncestralGhostID"; - summonMap[ESM::MagicEffect::SummonBonelord] = "sMagicBonelordID"; - summonMap[ESM::MagicEffect::SummonBonewalker] = "sMagicLeastBonewalkerID"; - summonMap[ESM::MagicEffect::SummonCenturionSphere] = "sMagicCenturionSphereID"; - summonMap[ESM::MagicEffect::SummonClannfear] = "sMagicClannfearID"; - summonMap[ESM::MagicEffect::SummonDaedroth] = "sMagicDaedrothID"; - summonMap[ESM::MagicEffect::SummonDremora] = "sMagicDremoraID"; - summonMap[ESM::MagicEffect::SummonFabricant] = "sMagicFabricantID"; - summonMap[ESM::MagicEffect::SummonFlameAtronach] = "sMagicFlameAtronachID"; - summonMap[ESM::MagicEffect::SummonFrostAtronach] = "sMagicFrostAtronachID"; - summonMap[ESM::MagicEffect::SummonGoldenSaint] = "sMagicGoldenSaintID"; - summonMap[ESM::MagicEffect::SummonGreaterBonewalker] = "sMagicGreaterBonewalkerID"; - summonMap[ESM::MagicEffect::SummonHunger] = "sMagicHungerID"; - summonMap[ESM::MagicEffect::SummonScamp] = "sMagicScampID"; - summonMap[ESM::MagicEffect::SummonSkeletalMinion] = "sMagicSkeletalMinionID"; - summonMap[ESM::MagicEffect::SummonStormAtronach] = "sMagicStormAtronachID"; - summonMap[ESM::MagicEffect::SummonWingedTwilight] = "sMagicWingedTwilightID"; - summonMap[ESM::MagicEffect::SummonWolf] = "sMagicCreature01ID"; - summonMap[ESM::MagicEffect::SummonBear] = "sMagicCreature02ID"; - summonMap[ESM::MagicEffect::SummonBonewolf] = "sMagicCreature03ID"; - summonMap[ESM::MagicEffect::SummonCreature04] = "sMagicCreature04ID"; - summonMap[ESM::MagicEffect::SummonCreature05] = "sMagicCreature05ID"; - } - - for (std::map::iterator it = summonMap.begin(); it != summonMap.end(); ++it) - { - std::map& creatureMap = creatureStats.getSummonedCreatureMap(); - bool found = creatureMap.find(it->first) != creatureMap.end(); - int magnitude = creatureStats.getMagicEffects().get(it->first).mMagnitude; - if (found != (magnitude > 0)) - { - if (magnitude > 0) - { - ESM::Position ipos = ptr.getRefData().getPosition(); - Ogre::Vector3 pos(ipos.pos); - Ogre::Quaternion rot(Ogre::Radian(-ipos.rot[2]), Ogre::Vector3::UNIT_Z); - const float distance = 50; - pos = pos + distance*rot.yAxis(); - ipos.pos[0] = pos.x; - ipos.pos[1] = pos.y; - ipos.pos[2] = pos.z; - ipos.rot[0] = 0; - ipos.rot[1] = 0; - ipos.rot[2] = 0; - - std::string creatureID = - MWBase::Environment::get().getWorld()->getStore().get().find(it->second)->getString(); - - if (!creatureID.empty()) - { - MWWorld::CellStore* store = ptr.getCell(); - MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), creatureID, 1); - ref.getPtr().getCellRef().setPosition(ipos); - - MWMechanics::CreatureStats& summonedCreatureStats = ref.getPtr().getClass().getCreatureStats(ref.getPtr()); - - // Make the summoned creature follow its master and help in fights - AiFollow package(ptr.getRefData().getHandle()); - summonedCreatureStats.getAiSequence().stack(package, ref.getPtr()); - int creatureActorId = summonedCreatureStats.getActorId(); - - MWWorld::Ptr placed = MWBase::Environment::get().getWorld()->safePlaceObject(ref.getPtr(),store,ipos); - - MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(placed); - if (anim) - { - const ESM::Static* fx = MWBase::Environment::get().getWorld()->getStore().get() - .search("VFX_Summon_Start"); - if (fx) - anim->addEffect("meshes\\" + fx->mModel, -1, false); - } - - creatureMap.insert(std::make_pair(it->first, creatureActorId)); - } - } - else - { - // Summon lifetime has expired. Try to delete the creature. - int actorId = creatureMap[it->first]; - creatureMap.erase(it->first); - - MWWorld::Ptr ptr = MWBase::Environment::get().getWorld()->searchPtrViaActorId(actorId); - if (!ptr.isEmpty()) - { - // TODO: Show death animation before deleting? We shouldn't allow looting the corpse while the animation - // plays though, which is a rather lame exploit in vanilla. - MWBase::Environment::get().getWorld()->deleteObject(ptr); - - const ESM::Static* fx = MWBase::Environment::get().getWorld()->getStore().get() - .search("VFX_Summon_End"); - if (fx) - MWBase::Environment::get().getWorld()->spawnEffect("meshes\\" + fx->mModel, - "", Ogre::Vector3(ptr.getRefData().getPosition().pos)); - } - else - { - // We didn't find the creature. It's probably in an inactive cell. - // Add to graveyard so we can delete it when the cell becomes active. - std::vector& graveyard = creatureStats.getSummonedCreatureGraveyard(); - graveyard.push_back(actorId); - } - } - } - } - - std::vector& graveyard = creatureStats.getSummonedCreatureGraveyard(); - for (std::vector::iterator it = graveyard.begin(); it != graveyard.end(); ) - { - MWWorld::Ptr ptr = MWBase::Environment::get().getWorld()->searchPtrViaActorId(*it); - if (!ptr.isEmpty()) - { - it = graveyard.erase(it); - - const ESM::Static* fx = MWBase::Environment::get().getWorld()->getStore().get() - .search("VFX_Summon_End"); - if (fx) - MWBase::Environment::get().getWorld()->spawnEffect("meshes\\" + fx->mModel, - "", Ogre::Vector3(ptr.getRefData().getPosition().pos)); - - MWBase::Environment::get().getWorld()->deleteObject(ptr); - } - else - ++it; - } + UpdateSummonedCreatures updateSummonedCreatures(ptr); + creatureStats.getActiveSpells().visitEffectSources(updateSummonedCreatures); + if (ptr.getClass().hasInventoryStore(ptr)) + ptr.getClass().getInventoryStore(ptr).visitEffectSources(updateSummonedCreatures); + updateSummonedCreatures.finish(); } - void Actors::calculateNpcStatModifiers (const MWWorld::Ptr& ptr) + void Actors::calculateNpcStatModifiers (const MWWorld::Ptr& ptr, float duration) { NpcStats &npcStats = ptr.getClass().getNpcStats(ptr); const MagicEffects &effects = npcStats.getMagicEffects(); @@ -706,21 +782,30 @@ namespace MWMechanics for(int i = 0;i < ESM::Skill::Length;++i) { SkillValue& skill = npcStats.getSkill(i); - skill.setModifier(effects.get(EffectKey(ESM::MagicEffect::FortifySkill, i)).mMagnitude - - effects.get(EffectKey(ESM::MagicEffect::DrainSkill, i)).mMagnitude - - effects.get(EffectKey(ESM::MagicEffect::AbsorbSkill, i)).mMagnitude); + skill.setModifiers(effects.get(EffectKey(ESM::MagicEffect::FortifySkill, i)).getMagnitude(), + effects.get(EffectKey(ESM::MagicEffect::DrainSkill, i)).getMagnitude(), + effects.get(EffectKey(ESM::MagicEffect::AbsorbSkill, i)).getMagnitude()); + + skill.damage(effects.get(EffectKey(ESM::MagicEffect::DamageSkill, i)).getMagnitude() * duration * 1.5); + skill.restore(effects.get(EffectKey(ESM::MagicEffect::RestoreSkill, i)).getMagnitude() * duration * 1.5); } } void Actors::updateDrowning(const MWWorld::Ptr& ptr, float duration) { - MWBase::World *world = MWBase::Environment::get().getWorld(); + PtrActorMap::iterator it = mActors.find(ptr); + if (it == mActors.end()) + return; + CharacterController* ctrl = it->second->getCharacterController(); + NpcStats &stats = ptr.getClass().getNpcStats(ptr); - if(world->isSubmerged(ptr) && - stats.getMagicEffects().get(ESM::MagicEffect::WaterBreathing).mMagnitude == 0) + MWBase::World *world = MWBase::Environment::get().getWorld(); + bool knockedOutUnderwater = (ctrl->isKnockedOut() && world->isUnderwater(ptr.getCell(), Ogre::Vector3(ptr.getRefData().getPosition().pos))); + if((world->isSubmerged(ptr) || knockedOutUnderwater) + && stats.getMagicEffects().get(ESM::MagicEffect::WaterBreathing).getMagnitude() == 0) { float timeLeft = 0.0f; - if(stats.getFatigue().getCurrent() == 0) + if(knockedOutUnderwater) stats.setTimeToStartDrowning(0); else { @@ -735,13 +820,13 @@ namespace MWMechanics static const float fSuffocationDamage = world->getStore().get().find("fSuffocationDamage")->getFloat(); ptr.getClass().setActorHealth(ptr, stats.getHealth().getCurrent() - fSuffocationDamage*duration); - // Play a drowning sound as necessary for the player + // Play a drowning sound + MWBase::SoundManager *sndmgr = MWBase::Environment::get().getSoundManager(); + if(!sndmgr->getSoundPlaying(ptr, "drown")) + sndmgr->playSound3D(ptr, "drown", 1.0f, 1.0f); + if(ptr == world->getPlayerPtr()) - { - MWBase::SoundManager *sndmgr = MWBase::Environment::get().getSoundManager(); - if(!sndmgr->getSoundPlaying(MWWorld::Ptr(), "drown")) - sndmgr->playSound("drown", 1.0f, 1.0f); - } + MWBase::Environment::get().getWindowManager()->activateHitOverlay(false); } } else @@ -777,7 +862,7 @@ namespace MWMechanics { if (torch != inventoryStore.end()) { - if (!ptr.getClass().getCreatureStats (ptr).isHostile()) + if (!ptr.getClass().getCreatureStats (ptr).getAiSequence().isInCombat()) { // For non-hostile NPCs, unequip whatever is in the left slot in favor of a light. if (heldIter != inventoryStore.end() && heldIter->getTypeName() != typeid(ESM::Light).name()) @@ -857,7 +942,10 @@ namespace MWMechanics CreatureStats& creatureStats = ptr.getClass().getCreatureStats(ptr); NpcStats& npcStats = ptr.getClass().getNpcStats(ptr); - if (ptr.getClass().isClass(ptr, "Guard") && creatureStats.getAiSequence().getTypeId() != AiPackage::TypeIdPursue && !creatureStats.isHostile()) + if (player.getClass().getNpcStats(player).isWerewolf()) + return; + + if (ptr.getClass().isClass(ptr, "Guard") && creatureStats.getAiSequence().getTypeId() != AiPackage::TypeIdPursue && !creatureStats.getAiSequence().isInCombat()) { const MWWorld::ESMStore& esmStore = MWBase::Environment::get().getWorld()->getStore(); int cutoff = esmStore.get().find("iCrimeThreshold")->getInt(); @@ -890,9 +978,9 @@ namespace MWMechanics creatureStats.getAiSequence().stopCombat(); // Reset factors to attack - creatureStats.setHostile(false); creatureStats.setAttacked(false); creatureStats.setAlarmed(false); + creatureStats.setAiSetting(CreatureStats::AI_Fight, ptr.getClass().getBaseFightRating(ptr)); // Update witness crime id npcStats.setCrimeId(-1); @@ -905,30 +993,22 @@ namespace MWMechanics Actors::~Actors() { - PtrControllerMap::iterator it(mActors.begin()); - for (; it != mActors.end(); ++it) - { - delete it->second; - it->second = NULL; - } + clear(); } void Actors::addActor (const MWWorld::Ptr& ptr, bool updateImmediately) { - // erase previous death events since we are currently only tracking them while in an active cell - ptr.getClass().getCreatureStats(ptr).clearHasDied(); - removeActor(ptr); MWRender::Animation *anim = MWBase::Environment::get().getWorld()->getAnimation(ptr); - mActors.insert(std::make_pair(ptr, new CharacterController(ptr, anim))); + mActors.insert(std::make_pair(ptr, new Actor(ptr, anim))); if (updateImmediately) - mActors[ptr]->update(0); + mActors[ptr]->getCharacterController()->update(0); } void Actors::removeActor (const MWWorld::Ptr& ptr) { - PtrControllerMap::iterator iter = mActors.find(ptr); + PtrActorMap::iterator iter = mActors.find(ptr); if(iter != mActors.end()) { delete iter->second; @@ -938,20 +1018,20 @@ namespace MWMechanics void Actors::updateActor(const MWWorld::Ptr &old, const MWWorld::Ptr &ptr) { - PtrControllerMap::iterator iter = mActors.find(old); + PtrActorMap::iterator iter = mActors.find(old); if(iter != mActors.end()) { - CharacterController *ctrl = iter->second; + Actor *actor = iter->second; mActors.erase(iter); - ctrl->updatePtr(ptr); - mActors.insert(std::make_pair(ptr, ctrl)); + actor->updatePtr(ptr); + mActors.insert(std::make_pair(ptr, actor)); } } void Actors::dropActors (const MWWorld::CellStore *cellStore, const MWWorld::Ptr& ignore) { - PtrControllerMap::iterator iter = mActors.begin(); + PtrActorMap::iterator iter = mActors.begin(); while(iter != mActors.end()) { if(iter->first.getCell()==cellStore && iter->first != ignore) @@ -964,69 +1044,76 @@ namespace MWMechanics } } - static Ogre::Vector3 sBasePoint; - bool comparePtrDist (const MWWorld::Ptr& ptr1, const MWWorld::Ptr& ptr2) - { - return (sBasePoint.squaredDistance(Ogre::Vector3(ptr1.getRefData().getPosition().pos)) - < sBasePoint.squaredDistance(Ogre::Vector3(ptr2.getRefData().getPosition().pos))); - } - void Actors::update (float duration, bool paused) { if(!paused) { - std::list listGuards; // at the moment only guards certainly will fight with creatures - static float timerUpdateAITargets = 0; + static float timerUpdateHeadTrack = 0; // target lists get updated once every 1.0 sec if (timerUpdateAITargets >= 1.0f) timerUpdateAITargets = 0; - - // Reset data from previous frame - for (PtrControllerMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) - { - // Reset last hit object, which is only valid for one frame - // Note, the new hit object for this frame may be set by CharacterController::update -> Animation::runAnimation - // (below) - iter->first.getClass().getCreatureStats(iter->first).setLastHitObject(std::string()); - - // add guards to list to later make them fight with creatures - if (timerUpdateAITargets == 0 && iter->first.getClass().isClass(iter->first, "Guard")) - listGuards.push_back(iter->first); - } + if (timerUpdateHeadTrack >= 0.3f) timerUpdateHeadTrack = 0; MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); - listGuards.push_back(player); + int hostilesCount = 0; // need to know this to play Battle music + + // AI processing is only done within distance of 7168 units to the player. Note the "AI distance" slider doesn't affect this + // (it only does some throttling for targets beyond the "AI distance", so doesn't give any guarantees as to whether AI will be enabled or not) + // This distance could be made configurable later, but the setting must be marked with a big warning: + // using higher values will make a quest in Bloodmoon harder or impossible to complete (bug #1876) + const float sqrProcessingDistance = 7168*7168; + + /// \todo move update logic to Actor class where appropriate // AI and magic effects update - for(PtrControllerMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) + for(PtrActorMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) { if (!iter->first.getClass().getCreatureStats(iter->first).isDead()) { updateActor(iter->first, duration); - - if (MWBase::Environment::get().getMechanicsManager()->isAIActive()) + if (MWBase::Environment::get().getMechanicsManager()->isAIActive() && + Ogre::Vector3(player.getRefData().getPosition().pos).squaredDistance(Ogre::Vector3(iter->first.getRefData().getPosition().pos)) + <= sqrProcessingDistance) { - // make guards and creatures fight each other - if (timerUpdateAITargets == 0 && iter->first.getTypeName() == typeid(ESM::Creature).name() && !listGuards.empty()) + if (timerUpdateAITargets == 0) { - sBasePoint = Ogre::Vector3(iter->first.getRefData().getPosition().pos); - listGuards.sort(comparePtrDist); // try to engage combat starting from the nearest guard - - for (std::list::iterator it = listGuards.begin(); it != listGuards.end(); ++it) + if (iter->first != player) + adjustCommandedActor(iter->first); + + for(PtrActorMap::iterator it(mActors.begin()); it != mActors.end(); ++it) { - engageCombat(iter->first, *it, *it == player); + if (it->first == iter->first || iter->first == player) // player is not AI-controlled + continue; + engageCombat(iter->first, it->first, it->first == player); } } + if (timerUpdateHeadTrack == 0) + { + float sqrHeadTrackDistance = std::numeric_limits::max(); + MWWorld::Ptr headTrackTarget; - if (iter->first != player) engageCombat(iter->first, player, true); + for(PtrActorMap::iterator it(mActors.begin()); it != mActors.end(); ++it) + { + if (it->first == iter->first) + continue; + updateHeadTracking(iter->first, it->first, headTrackTarget, sqrHeadTrackDistance); + } + iter->second->getCharacterController()->setHeadTrackTarget(headTrackTarget); + } if (iter->first.getClass().isNpc() && iter->first != player) updateCrimePersuit(iter->first, duration); if (iter->first != player) - iter->first.getClass().getCreatureStats(iter->first).getAiSequence().execute(iter->first, duration); + { + CreatureStats &stats = iter->first.getClass().getCreatureStats(iter->first); + if (isConscious(iter->first)) + stats.getAiSequence().execute(iter->first,iter->second->getAiState(), duration); + + if (stats.getAiSequence().isInCombat() && !stats.isDead()) hostilesCount++; + } } if(iter->first.getTypeName() == typeid(ESM::NPC).name()) @@ -1035,25 +1122,43 @@ namespace MWMechanics } timerUpdateAITargets += duration; + timerUpdateHeadTrack += duration; // Looping magic VFX update // Note: we need to do this before any of the animations are updated. // Reaching the text keys may trigger Hit / Spellcast (and as such, particles), // so updating VFX immediately after that would just remove the particle effects instantly. // There needs to be a magic effect update in between. - for(PtrControllerMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) - iter->second->updateContinuousVfx(); + for(PtrActorMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) + iter->second->getCharacterController()->updateContinuousVfx(); // Animation/movement update - for(PtrControllerMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) + CharacterController* playerCharacter = NULL; + for(PtrActorMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) { + if (iter->first != player && + Ogre::Vector3(player.getRefData().getPosition().pos).squaredDistance(Ogre::Vector3(iter->first.getRefData().getPosition().pos)) + > sqrProcessingDistance) + continue; + if (iter->first.getClass().getCreatureStats(iter->first).getMagicEffects().get( - ESM::MagicEffect::Paralyze).mMagnitude > 0) - iter->second->skipAnim(); - iter->second->update(duration); + ESM::MagicEffect::Paralyze).getMagnitude() > 0) + iter->second->getCharacterController()->skipAnim(); + + // Handle player last, in case a cell transition occurs by casting a teleportation spell + // (would invalidate the iterator) + if (iter->first.getCellRef().getRefId() == "player") + { + playerCharacter = iter->second->getCharacterController(); + continue; + } + iter->second->getCharacterController()->update(duration); } - for(PtrControllerMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) + if (playerCharacter) + playerCharacter->update(duration); + + for(PtrActorMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) { const MWWorld::Class &cls = iter->first.getClass(); CreatureStats &stats = cls.getCreatureStats(iter->first); @@ -1061,53 +1166,43 @@ namespace MWMechanics //KnockedOutOneFrameLogic //Used for "OnKnockedOut" command //Put here to ensure that it's run for PRECISELY one frame. - if (stats.getKnockedDown() && !stats.getKnockedDownOneFrame() && !stats.getKnockedDownOverOneFrame()) { //Start it for one frame if nessesary + if (stats.getKnockedDown() && !stats.getKnockedDownOneFrame() && !stats.getKnockedDownOverOneFrame()) + { //Start it for one frame if nessesary stats.setKnockedDownOneFrame(true); } - else if (stats.getKnockedDownOneFrame() && !stats.getKnockedDownOverOneFrame()) { //Turn off KnockedOutOneframe + else if (stats.getKnockedDownOneFrame() && !stats.getKnockedDownOverOneFrame()) + { //Turn off KnockedOutOneframe stats.setKnockedDownOneFrame(false); stats.setKnockedDownOverOneFrame(true); } } - int hostilesCount = 0; // need to know this to play Battle music - - for(PtrControllerMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) - { - const MWWorld::Class &cls = iter->first.getClass(); - CreatureStats &stats = cls.getCreatureStats(iter->first); - - if(!stats.isDead()) - { - if (stats.isHostile()) hostilesCount++; - } - } - killDeadActors(); // check if we still have any player enemies to switch music static bool isBattleMusic = false; - if (isBattleMusic && hostilesCount == 0) + if (isBattleMusic && hostilesCount == 0 && !(player.getClass().getCreatureStats(player).isDead() && + MWBase::Environment::get().getSoundManager()->isMusicPlaying())) { MWBase::Environment::get().getSoundManager()->playPlaylist(std::string("Explore")); isBattleMusic = false; } - else if (!isBattleMusic && hostilesCount > 0) + else if (!isBattleMusic && hostilesCount > 0) { MWBase::Environment::get().getSoundManager()->playPlaylist(std::string("Battle")); isBattleMusic = true; } static float sneakTimer = 0.f; // times update of sneak icon - static float sneakSkillTimer = 0.f; // times sneak skill progress from "avoid notice" // if player is in sneak state see if anyone detects him if (player.getClass().getCreatureStats(player).getMovementFlag(MWMechanics::CreatureStats::Flag_Sneak)) { + static float sneakSkillTimer = 0.f; // times sneak skill progress from "avoid notice" + const MWWorld::ESMStore& esmStore = MWBase::Environment::get().getWorld()->getStore(); const int radius = esmStore.get().find("fSneakUseDist")->getInt(); - bool detected = false; static float fSneakUseDelay = esmStore.get().find("fSneakUseDelay")->getFloat(); @@ -1119,7 +1214,9 @@ namespace MWMechanics // Set when an NPC is within line of sight and distance, but is still unaware. Used for skill progress. bool avoidedNotice = false; - for (PtrControllerMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) + bool detected = false; + + for (PtrActorMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) { if (iter->first == player) // not the player continue; @@ -1162,45 +1259,32 @@ namespace MWMechanics void Actors::killDeadActors() { - for(PtrControllerMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) + for(PtrActorMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) { const MWWorld::Class &cls = iter->first.getClass(); CreatureStats &stats = cls.getCreatureStats(iter->first); if(!stats.isDead()) { - if(iter->second->isDead()) + if(iter->second->getCharacterController()->isDead()) { // Actor has been resurrected. Notify the CharacterController and re-enable collision. MWBase::Environment::get().getWorld()->enableActorCollision(iter->first, true); - iter->second->resurrect(); + iter->second->getCharacterController()->resurrect(); } if(!stats.isDead()) continue; } - // If it's the player and God Mode is turned on, keep it alive - if (iter->first.getRefData().getHandle()=="player" && - MWBase::Environment::get().getWorld()->getGodModeState()) + if (iter->second->getCharacterController()->kill()) { - MWMechanics::DynamicStat stat (stats.getHealth()); + iter->first.getClass().getCreatureStats(iter->first).notifyDied(); - if (stat.getModified()<1) - { - stat.setModified(1, 0); - stats.setHealth(stat); - } - stats.resurrect(); - continue; - } - - if (iter->second->kill()) - { ++mDeathCount[Misc::StringUtils::lowerCase(iter->first.getCellRef().getRefId())]; // Make sure spell effects with CasterLinked flag are removed - for (PtrControllerMap::iterator iter2(mActors.begin());iter2 != mActors.end();++iter2) + for (PtrActorMap::iterator iter2(mActors.begin());iter2 != mActors.end();++iter2) { MWMechanics::ActiveSpells& spells = iter2->first.getClass().getCreatureStats(iter2->first).getActiveSpells(); spells.purge(stats.getActorId()); @@ -1215,7 +1299,7 @@ namespace MWMechanics // Reset magic effects and recalculate derived effects // One case where we need this is to make sure bound items are removed upon death - stats.setMagicEffects(MWMechanics::MagicEffects()); + stats.modifyMagicEffects(MWMechanics::MagicEffects()); stats.getActiveSpells().clear(); calculateCreatureStatModifiers(iter->first, 0); @@ -1229,7 +1313,7 @@ namespace MWMechanics void Actors::restoreDynamicStats(bool sleep) { - for(PtrControllerMap::iterator iter(mActors.begin());iter != mActors.end();++iter) + for(PtrActorMap::iterator iter(mActors.begin());iter != mActors.end();++iter) restoreDynamicStats(iter->first, sleep); } @@ -1261,35 +1345,35 @@ namespace MWMechanics void Actors::forceStateUpdate(const MWWorld::Ptr & ptr) { - PtrControllerMap::iterator iter = mActors.find(ptr); + PtrActorMap::iterator iter = mActors.find(ptr); if(iter != mActors.end()) - iter->second->forceStateUpdate(); + iter->second->getCharacterController()->forceStateUpdate(); } void Actors::playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number) { - PtrControllerMap::iterator iter = mActors.find(ptr); + PtrActorMap::iterator iter = mActors.find(ptr); if(iter != mActors.end()) - iter->second->playGroup(groupName, mode, number); + iter->second->getCharacterController()->playGroup(groupName, mode, number); } void Actors::skipAnimation(const MWWorld::Ptr& ptr) { - PtrControllerMap::iterator iter = mActors.find(ptr); + PtrActorMap::iterator iter = mActors.find(ptr); if(iter != mActors.end()) - iter->second->skipAnim(); + iter->second->getCharacterController()->skipAnim(); } bool Actors::checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string& groupName) { - PtrControllerMap::iterator iter = mActors.find(ptr); + PtrActorMap::iterator iter = mActors.find(ptr); if(iter != mActors.end()) - return iter->second->isAnimPlaying(groupName); + return iter->second->getCharacterController()->isAnimPlaying(groupName); return false; } void Actors::getObjectsInRange(const Ogre::Vector3& position, float radius, std::vector& out) { - for (PtrControllerMap::iterator iter = mActors.begin(); iter != mActors.end(); ++iter) + for (PtrActorMap::iterator iter = mActors.begin(); iter != mActors.end(); ++iter) { if (Ogre::Vector3(iter->first.getRefData().getPosition().pos).squaredDistance(position) <= radius*radius) out.push_back(iter->first); @@ -1299,15 +1383,58 @@ namespace MWMechanics std::list Actors::getActorsFollowing(const MWWorld::Ptr& actor) { std::list list; - for(PtrControllerMap::iterator iter(mActors.begin());iter != mActors.end();iter++) + for(PtrActorMap::iterator iter(mActors.begin());iter != mActors.end();++iter) { const MWWorld::Class &cls = iter->first.getClass(); CreatureStats &stats = cls.getCreatureStats(iter->first); - if(!stats.isDead() && stats.getAiSequence().getTypeId() == AiPackage::TypeIdFollow) + if (stats.isDead()) + continue; + + // An actor counts as following if AiFollow is the current AiPackage, or there are only Combat packages before the AiFollow package + for (std::list::const_iterator it = stats.getAiSequence().begin(); it != stats.getAiSequence().end(); ++it) { - MWMechanics::AiFollow* package = static_cast(stats.getAiSequence().getActivePackage()); - if(package->getFollowedActor() == actor.getCellRef().getRefId()) - list.push_front(iter->first); + if ((*it)->getTypeId() == MWMechanics::AiPackage::TypeIdFollow) + { + MWWorld::Ptr followTarget = static_cast(*it)->getTarget(); + if (followTarget.isEmpty()) + continue; + if (followTarget == actor) + list.push_back(iter->first); + else + break; + } + else if ((*it)->getTypeId() != MWMechanics::AiPackage::TypeIdCombat) + break; + } + } + return list; + } + + std::list Actors::getActorsFollowingIndices(const MWWorld::Ptr &actor) + { + std::list list; + for(PtrActorMap::iterator iter(mActors.begin());iter != mActors.end();++iter) + { + const MWWorld::Class &cls = iter->first.getClass(); + CreatureStats &stats = cls.getCreatureStats(iter->first); + if (stats.isDead()) + continue; + + // An actor counts as following if AiFollow is the current AiPackage, or there are only Combat packages before the AiFollow package + for (std::list::const_iterator it = stats.getAiSequence().begin(); it != stats.getAiSequence().end(); ++it) + { + if ((*it)->getTypeId() == MWMechanics::AiPackage::TypeIdFollow) + { + MWWorld::Ptr followTarget = static_cast(*it)->getTarget(); + if (followTarget.isEmpty()) + continue; + if (followTarget == actor) + list.push_back(static_cast(*it)->getFollowIndex()); + else + break; + } + else if ((*it)->getTypeId() != MWMechanics::AiPackage::TypeIdCombat) + break; } } return list; @@ -1320,7 +1447,7 @@ namespace MWMechanics getObjectsInRange(position, MWBase::Environment::get().getWorld()->getStore().get().find("fAlarmRadius")->getFloat(), neighbors); //only care about those within the alarm disance - for(std::vector::iterator iter(neighbors.begin());iter != neighbors.end();iter++) + for(std::vector::iterator iter(neighbors.begin());iter != neighbors.end();++iter) { const MWWorld::Class &cls = iter->getClass(); CreatureStats &stats = cls.getCreatureStats(*iter); @@ -1339,11 +1466,9 @@ namespace MWMechanics writer.writeHNT ("COUN", it->second); } writer.endRecord(ESM::REC_DCOU); - - listener.increaseProgress(1); } - void Actors::readRecord (ESM::ESMReader& reader, int32_t type) + void Actors::readRecord (ESM::ESMReader& reader, uint32_t type) { if (type == ESM::REC_DCOU) { @@ -1359,6 +1484,49 @@ namespace MWMechanics void Actors::clear() { + PtrActorMap::iterator it(mActors.begin()); + for (; it != mActors.end(); ++it) + { + delete it->second; + it->second = NULL; + } + mActors.clear(); mDeathCount.clear(); } + + void Actors::updateMagicEffects(const MWWorld::Ptr &ptr) + { + adjustMagicEffects(ptr); + calculateCreatureStatModifiers(ptr, 0.f); + if (ptr.getClass().isNpc()) + calculateNpcStatModifiers(ptr, 0.f); + } + + bool Actors::isReadyToBlock(const MWWorld::Ptr &ptr) const + { + PtrActorMap::const_iterator it = mActors.find(ptr); + if (it == mActors.end()) + return false; + + return it->second->getCharacterController()->isReadyToBlock(); + } + + void Actors::fastForwardAi() + { + if (!MWBase::Environment::get().getMechanicsManager()->isAIActive()) + return; + + // making a copy since fast-forward could move actor to a different cell and invalidate the mActors iterator + PtrActorMap map = mActors; + for (PtrActorMap::iterator it = map.begin(); it != map.end(); ++it) + { + MWWorld::Ptr ptr = it->first; + if (ptr == MWBase::Environment::get().getWorld()->getPlayerPtr() + || !isConscious(ptr) + || ptr.getClass().getCreatureStats(ptr).getMagicEffects().get(ESM::MagicEffect::Paralyze).getMagnitude() > 0) + continue; + MWMechanics::AiSequence& seq = ptr.getClass().getCreatureStats(ptr).getAiSequence(); + seq.fastForward(ptr, it->second->getAiState()); + } + } } diff --git a/apps/openmw/mwmechanics/actors.hpp b/apps/openmw/mwmechanics/actors.hpp index 4b5d77a7f..70f1b47d9 100644 --- a/apps/openmw/mwmechanics/actors.hpp +++ b/apps/openmw/mwmechanics/actors.hpp @@ -5,8 +5,8 @@ #include #include #include +#include -#include "character.hpp" #include "movement.hpp" #include "../mwbase/world.hpp" @@ -23,6 +23,8 @@ namespace MWWorld namespace MWMechanics { + class Actor; + class Actors { std::map mDeathCount; @@ -34,7 +36,7 @@ namespace MWMechanics void calculateDynamicStats (const MWWorld::Ptr& ptr); void calculateCreatureStatModifiers (const MWWorld::Ptr& ptr, float duration); - void calculateNpcStatModifiers (const MWWorld::Ptr& ptr); + void calculateNpcStatModifiers (const MWWorld::Ptr& ptr, float duration); void calculateRestoration (const MWWorld::Ptr& ptr, float duration); @@ -44,19 +46,21 @@ namespace MWMechanics void updateCrimePersuit (const MWWorld::Ptr& ptr, float duration); + void killDeadActors (); + public: Actors(); ~Actors(); - typedef std::map PtrControllerMap; + typedef std::map PtrActorMap; - PtrControllerMap::const_iterator begin() { return mActors.begin(); } - PtrControllerMap::const_iterator end() { return mActors.end(); } + PtrActorMap::const_iterator begin() { return mActors.begin(); } + PtrActorMap::const_iterator end() { return mActors.end(); } /// Update magic effects for an actor. Usually done automatically once per frame, but if we're currently /// paused we may want to do it manually (after equipping permanent enchantment) - void updateMagicEffects (const MWWorld::Ptr& ptr) { adjustMagicEffects(ptr); } + void updateMagicEffects (const MWWorld::Ptr& ptr); void addActor (const MWWorld::Ptr& ptr, bool updateImmediately=false); ///< Register an actor for stats management @@ -87,6 +91,9 @@ namespace MWMechanics */ void engageCombat(const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2, bool againstPlayer); + void updateHeadTracking(const MWWorld::Ptr& actor, const MWWorld::Ptr& targetActor, + MWWorld::Ptr& headTrackTarget, float& sqrHeadTrackDistance); + void restoreDynamicStats(bool sleep); ///< If the player is sleeping, this should be called every hour. @@ -95,12 +102,12 @@ namespace MWMechanics int getHoursToRest(const MWWorld::Ptr& ptr) const; ///< Calculate how many hours the given actor needs to rest in order to be fully healed + void fastForwardAi(); + ///< Simulate the passing of time + int countDeaths (const std::string& id) const; ///< Return the number of deaths for actors with the given ID. - ///@see MechanicsManager::killDeadActors - void killDeadActors (); - void forceStateUpdate(const MWWorld::Ptr &ptr); void playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number); @@ -113,18 +120,23 @@ namespace MWMechanics /**ie AiFollow is active and the target is the actor **/ std::list getActorsFollowing(const MWWorld::Ptr& actor); + /// Get the list of AiFollow::mFollowIndex for all actors following this target + std::list getActorsFollowingIndices(const MWWorld::Ptr& actor); + ///Returns the list of actors which are fighting the given actor /**ie AiCombat is active and the target is the actor **/ std::list getActorsFighting(const MWWorld::Ptr& actor); void write (ESM::ESMWriter& writer, Loading::Listener& listener) const; - void readRecord (ESM::ESMReader& reader, int32_t type); + void readRecord (ESM::ESMReader& reader, uint32_t type); void clear(); // Clear death counter + bool isReadyToBlock(const MWWorld::Ptr& ptr) const; + private: - PtrControllerMap mActors; + PtrActorMap mActors; }; } diff --git a/apps/openmw/mwmechanics/aiactivate.cpp b/apps/openmw/mwmechanics/aiactivate.cpp index 9e01c3fe7..9e25084d3 100644 --- a/apps/openmw/mwmechanics/aiactivate.cpp +++ b/apps/openmw/mwmechanics/aiactivate.cpp @@ -5,6 +5,8 @@ #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" +#include "../mwmechanics/creaturestats.hpp" + #include "../mwworld/class.hpp" #include "../mwworld/cellstore.hpp" @@ -19,12 +21,17 @@ MWMechanics::AiActivate *MWMechanics::AiActivate::clone() const { return new AiActivate(*this); } -bool MWMechanics::AiActivate::execute (const MWWorld::Ptr& actor,float duration) +bool MWMechanics::AiActivate::execute (const MWWorld::Ptr& actor, AiState& state, float duration) { ESM::Position pos = actor.getRefData().getPosition(); //position of the actor const MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtr(mObjectId, false); //The target to follow - if(target == MWWorld::Ptr()) + actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Nothing); + + if(target == MWWorld::Ptr() || + !target.getRefData().getCount() || !target.getRefData().isEnabled() // Really we should be checking whether the target is currently registered + // with the MechanicsManager + ) return true; //Target doesn't exist //Set the target desition from the actor diff --git a/apps/openmw/mwmechanics/aiactivate.hpp b/apps/openmw/mwmechanics/aiactivate.hpp index 8003c2a36..e25afe285 100644 --- a/apps/openmw/mwmechanics/aiactivate.hpp +++ b/apps/openmw/mwmechanics/aiactivate.hpp @@ -28,15 +28,13 @@ namespace MWMechanics AiActivate(const ESM::AiSequence::AiActivate* activate); virtual AiActivate *clone() const; - virtual bool execute (const MWWorld::Ptr& actor,float duration); + virtual bool execute (const MWWorld::Ptr& actor, AiState& state, float duration); virtual int getTypeId() const; virtual void writeState(ESM::AiSequence::AiSequence& sequence) const; private: std::string mObjectId; - int mCellX; - int mCellY; }; } #endif // GAME_MWMECHANICS_AIACTIVATE_H diff --git a/apps/openmw/mwmechanics/aiavoiddoor.cpp b/apps/openmw/mwmechanics/aiavoiddoor.cpp index bab8bca28..b9954337d 100644 --- a/apps/openmw/mwmechanics/aiavoiddoor.cpp +++ b/apps/openmw/mwmechanics/aiavoiddoor.cpp @@ -18,7 +18,7 @@ MWMechanics::AiAvoidDoor::AiAvoidDoor(const MWWorld::Ptr& doorPtr) } -bool MWMechanics::AiAvoidDoor::execute (const MWWorld::Ptr& actor,float duration) +bool MWMechanics::AiAvoidDoor::execute (const MWWorld::Ptr& actor, AiState& state, float duration) { ESM::Position pos = actor.getRefData().getPosition(); @@ -51,17 +51,20 @@ bool MWMechanics::AiAvoidDoor::execute (const MWWorld::Ptr& actor,float duration ESM::Position tPos = mDoorPtr.getRefData().getPosition(); //Position of the door float x = pos.pos[0] - tPos.pos[0]; float y = pos.pos[1] - tPos.pos[1]; - float dirToDoor = std::atan2(x,y) + pos.rot[2] + mAdjAngle; //Calculates the direction to the door, relative to the direction of the NPC - // For example, if the NPC is directly facing the door this will be pi/2 - // Make actor move away from the door - actor.getClass().getMovementSettings(actor).mPosition[1] = -1 * std::sin(dirToDoor); //I knew I'd use trig someday - actor.getClass().getMovementSettings(actor).mPosition[0] = -1 * std::cos(dirToDoor); + actor.getClass().getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, true); - //Make all nearby actors also avoid the door + // Turn away from the door and move when turn completed + if (zTurn(actor, Ogre::Radian(std::atan2(x,y) + mAdjAngle), Ogre::Degree(5))) + actor.getClass().getMovementSettings(actor).mPosition[1] = 1; + else + actor.getClass().getMovementSettings(actor).mPosition[1] = 0; + actor.getClass().getMovementSettings(actor).mPosition[0] = 0; + + // Make all nearby actors also avoid the door std::vector actors; MWBase::Environment::get().getMechanicsManager()->getActorsInRange(Ogre::Vector3(pos.pos[0],pos.pos[1],pos.pos[2]),100,actors); - for(std::vector::iterator it = actors.begin(); it != actors.end(); it++) { + for(std::vector::iterator it = actors.begin(); it != actors.end(); ++it) { if(*it != MWBase::Environment::get().getWorld()->getPlayerPtr()) { //Not the player MWMechanics::AiSequence& seq = it->getClass().getCreatureStats(*it).getAiSequence(); if(seq.getTypeId() != MWMechanics::AiPackage::TypeIdAvoidDoor) { //Only add it once diff --git a/apps/openmw/mwmechanics/aiavoiddoor.hpp b/apps/openmw/mwmechanics/aiavoiddoor.hpp index 2374fbc1b..7590c8fcb 100644 --- a/apps/openmw/mwmechanics/aiavoiddoor.hpp +++ b/apps/openmw/mwmechanics/aiavoiddoor.hpp @@ -20,7 +20,7 @@ namespace MWMechanics virtual AiAvoidDoor *clone() const; - virtual bool execute (const MWWorld::Ptr& actor,float duration); + virtual bool execute (const MWWorld::Ptr& actor, AiState& state, float duration); virtual int getTypeId() const; diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index c0a97a1b1..fd4fd293d 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -22,14 +22,11 @@ #include "movement.hpp" #include "character.hpp" // fixme: for getActiveWeapon +#include "aicombataction.hpp" +#include "combat.hpp" + namespace { - static float sgn(Ogre::Radian a) - { - if(a.valueDegrees() > 0) - return 1.0; - return -1.0; - } //chooses an attack depending on probability to avoid uniformity ESM::Weapon::AttackType chooseBestAttack(const ESM::Weapon* weapon, MWMechanics::Movement &movement); @@ -39,16 +36,15 @@ namespace Ogre::Vector3 AimDirToMovingTarget(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, const Ogre::Vector3& vLastTargetPos, float duration, int weapType, float strength); - float getZAngleToDir(const Ogre::Vector3& dir, float dirLen = 0.0f) + float getZAngleToDir(const Ogre::Vector3& dir) { - float len = (dirLen > 0.0f)? dirLen : dir.length(); - return Ogre::Radian( Ogre::Math::ACos(dir.y / len) * sgn(Ogre::Math::ASin(dir.x / len)) ).valueDegrees(); + return Ogre::Math::ATan2(dir.x,dir.y).valueDegrees(); } float getXAngleToDir(const Ogre::Vector3& dir, float dirLen = 0.0f) { float len = (dirLen > 0.0f)? dirLen : dir.length(); - return Ogre::Radian(-Ogre::Math::ASin(dir.z / len)).valueDegrees(); + return -Ogre::Math::ASin(dir.z / len).valueDegrees(); } @@ -62,7 +58,7 @@ namespace // magnitude of pits/obstacles is defined by PATHFIND_Z_REACH bool checkWayIsClear(const Ogre::Vector3& from, const Ogre::Vector3& to, float offsetXY) { - if((to - from).length() >= PATHFIND_CAUTION_DIST || abs(from.z - to.z) <= PATHFIND_Z_REACH) + if((to - from).length() >= PATHFIND_CAUTION_DIST || std::abs(from.z - to.z) <= PATHFIND_Z_REACH) { Ogre::Vector3 dir = to - from; dir.z = 0; @@ -73,7 +69,7 @@ namespace // cast up-down ray and find height in world space of hit float h = _from.z - MWBase::Environment::get().getWorld()->getDistToNearestRayHit(_from, -Ogre::Vector3::UNIT_Z, verticalOffset + PATHFIND_Z_REACH + 1); - if(abs(from.z - h) <= PATHFIND_Z_REACH) + if(std::abs(from.z - h) <= PATHFIND_Z_REACH) return true; } @@ -86,39 +82,60 @@ namespace MWMechanics static const float DOOR_CHECK_INTERVAL = 1.5f; // same as AiWander // NOTE: MIN_DIST_TO_DOOR_SQUARED is defined in obstacle.hpp + + /// \brief This class holds the variables AiCombat needs which are deleted if the package becomes inactive. + struct AiCombatStorage : AiTemporaryBase + { + float mTimerAttack; + float mTimerReact; + float mTimerCombatMove; + bool mReadyToAttack; + bool mAttack; + bool mFollowTarget; + bool mCombatMove; + Ogre::Vector3 mLastTargetPos; + const MWWorld::CellStore* mCell; + boost::shared_ptr mCurrentAction; + float mActionCooldown; + float mStrength; + float mMinMaxAttackDuration[3][2]; + bool mMinMaxAttackDurationInitialised; + bool mForceNoShortcut; + ESM::Position mShortcutFailPos; + Ogre::Vector3 mLastActorPos; + MWMechanics::Movement mMovement; + + AiCombatStorage(): + mTimerAttack(0), + mTimerReact(0), + mTimerCombatMove(0), + mAttack(false), + mFollowTarget(false), + mCombatMove(false), + mReadyToAttack(false), + mForceNoShortcut(false), + mCell(NULL), + mCurrentAction(), + mActionCooldown(0), + mStrength(), + mMinMaxAttackDurationInitialised(false), + mLastTargetPos(0,0,0), + mLastActorPos(0,0,0), + mMovement(){} + }; + AiCombat::AiCombat(const MWWorld::Ptr& actor) : mTargetActorId(actor.getClass().getCreatureStats(actor).getActorId()) - , mMinMaxAttackDuration() - , mMovement() - { - init(); - - mLastTargetPos = Ogre::Vector3(actor.getRefData().getPosition().pos); - } + {} AiCombat::AiCombat(const ESM::AiSequence::AiCombat *combat) - : mMinMaxAttackDuration() - , mMovement() { mTargetActorId = combat->mTargetActorId; - - init(); } void AiCombat::init() { - mTimerAttack = 0; - mTimerReact = 0; - mTimerCombatMove = 0; - mFollowTarget = false; - mReadyToAttack = false; - mAttack = false; - mCombatMove = false; - mForceNoShortcut = false; - mStrength = 0; - mCell = NULL; - mLastTargetPos = Ogre::Vector3(0,0,0); - mMinMaxAttackDurationInitialised = false; + } /* @@ -167,57 +184,57 @@ namespace MWMechanics * Use the Observer Pattern to co-ordinate attacks, provide intelligence on * whether the target was hit, etc. */ - bool AiCombat::execute (const MWWorld::Ptr& actor,float duration) + bool AiCombat::execute (const MWWorld::Ptr& actor, AiState& state, float duration) { + // get or create temporary storage + AiCombatStorage& storage = state.get(); + + + //General description if(actor.getClass().getCreatureStats(actor).isDead()) return true; MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtrViaActorId(mTargetActorId); + if (target.isEmpty()) + return false; - if(target.getClass().getCreatureStats(target).isDead()) + if(!target.getRefData().getCount() || !target.getRefData().isEnabled() // Really we should be checking whether the target is currently registered + // with the MechanicsManager + || target.getClass().getCreatureStats(target).isDead()) return true; const MWWorld::Class& actorClass = actor.getClass(); MWBase::World* world = MWBase::Environment::get().getWorld(); - if (!actorClass.isNpc() && - // 1. pure water creature and Player moved out of water - ((target == world->getPlayerPtr() && - actorClass.canSwim(actor) && !actor.getClass().canWalk(actor) && !world->isSwimming(target)) - // 2. creature can't swim to target - || (!actorClass.canSwim(actor) && world->isSwimming(target)))) - { - if (target == world->getPlayerPtr()) - actorClass.getCreatureStats(actor).setHostile(false); - actorClass.getCreatureStats(actor).setAttackingOrSpell(false); - return true; - } //Update every frame - if(mCombatMove) + bool& combatMove = storage.mCombatMove; + float& timerCombatMove = storage.mTimerCombatMove; + MWMechanics::Movement& movement = storage.mMovement; + if(combatMove) { - mTimerCombatMove -= duration; - if( mTimerCombatMove <= 0) + timerCombatMove -= duration; + if( timerCombatMove <= 0) { - mTimerCombatMove = 0; - mMovement.mPosition[1] = mMovement.mPosition[0] = 0; - mCombatMove = false; + timerCombatMove = 0; + movement.mPosition[1] = movement.mPosition[0] = 0; + combatMove = false; } } - actorClass.getMovementSettings(actor) = mMovement; + actorClass.getMovementSettings(actor) = movement; actorClass.getMovementSettings(actor).mRotation[0] = 0; actorClass.getMovementSettings(actor).mRotation[2] = 0; - if(mMovement.mRotation[2] != 0) + if(movement.mRotation[2] != 0) { - if(zTurn(actor, Ogre::Degree(mMovement.mRotation[2]))) mMovement.mRotation[2] = 0; + if(zTurn(actor, Ogre::Degree(movement.mRotation[2]))) movement.mRotation[2] = 0; } - if(mMovement.mRotation[0] != 0) + if(movement.mRotation[0] != 0) { - if(smoothTurn(actor, Ogre::Degree(mMovement.mRotation[0]), 0)) mMovement.mRotation[0] = 0; + if(smoothTurn(actor, Ogre::Degree(movement.mRotation[0]), 0)) movement.mRotation[0] = 0; } //TODO: Some skills affect period of strikes.For berserk-like style period ~ 0.25f @@ -225,57 +242,96 @@ namespace MWMechanics ESM::Weapon::AttackType attackType; - if(mReadyToAttack) + + + + bool& attack = storage.mAttack; + bool& readyToAttack = storage.mReadyToAttack; + float& timerAttack = storage.mTimerAttack; + + bool& minMaxAttackDurationInitialised = storage.mMinMaxAttackDurationInitialised; + float (&minMaxAttackDuration)[3][2] = storage.mMinMaxAttackDuration; + + if(readyToAttack) { - if (!mMinMaxAttackDurationInitialised) + if (!minMaxAttackDurationInitialised) { // TODO: this must be updated when a different weapon is equipped - getMinMaxAttackDuration(actor, mMinMaxAttackDuration); - mMinMaxAttackDurationInitialised = true; + getMinMaxAttackDuration(actor, minMaxAttackDuration); + minMaxAttackDurationInitialised = true; } - if (mTimerAttack < 0) mAttack = false; + if (timerAttack < 0) attack = false; - mTimerAttack -= duration; + timerAttack -= duration; } else { - mTimerAttack = -attacksPeriod; - mAttack = false; + timerAttack = -attacksPeriod; + attack = false; } - actorClass.getCreatureStats(actor).setAttackingOrSpell(mAttack); + actorClass.getCreatureStats(actor).setAttackingOrSpell(attack); + + float& actionCooldown = storage.mActionCooldown; + actionCooldown -= duration; + + float& timerReact = storage.mTimerReact; float tReaction = 0.25f; - if(mTimerReact < tReaction) + if(timerReact < tReaction) { - mTimerReact += duration; + timerReact += duration; return false; } //Update with period = tReaction - mTimerReact = 0; + // Stop attacking if target is not seen + if (target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::Invisibility).getMagnitude() > 0 + || target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::Chameleon).getMagnitude() > 75) + { + movement.mPosition[1] = movement.mPosition[0] = 0; + return false; // TODO: run away instead of doing nothing + } - bool cellChange = mCell && (actor.getCell() != mCell); - if(!mCell || cellChange) + timerReact = 0; + const MWWorld::CellStore*& currentCell = storage.mCell; + bool cellChange = currentCell && (actor.getCell() != currentCell); + if(!currentCell || cellChange) { - mCell = actor.getCell(); + currentCell = actor.getCell(); } - const ESM::Weapon *weapon = NULL; - MWMechanics::WeaponType weaptype; - float weapRange = 1.0f; + MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(actor); + if (!anim) // shouldn't happen + return false; actorClass.getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, true); + if (actionCooldown > 0) + return false; + + float rangeAttack = 0; + float rangeFollow = 0; + boost::shared_ptr& currentAction = storage.mCurrentAction; + if (anim->upperBodyReady()) + { + currentAction = prepareNextAction(actor, target); + actionCooldown = currentAction->getActionCooldown(); + } + + if (currentAction.get()) + currentAction->getCombatRange(rangeAttack, rangeFollow); + + // FIXME: consider moving this stuff to ActionWeapon::getCombatRange + const ESM::Weapon *weapon = NULL; + MWMechanics::WeaponType weaptype = WeapType_None; + float weapRange = 1.0f; + // Get weapon characteristics if (actorClass.hasInventoryStore(actor)) { - MWMechanics::DrawState_ state = actorClass.getCreatureStats(actor).getDrawState(); - if (state == MWMechanics::DrawState_Spell || state == MWMechanics::DrawState_Nothing) - actorClass.getCreatureStats(actor).setDrawState(MWMechanics::DrawState_Weapon); - // TODO: Check equipped weapon and equip a different one if we can't attack with it // (e.g. no ammunition, or wrong type of ammunition equipped, etc. autoEquip is not very smart in this regard)) @@ -285,11 +341,11 @@ namespace MWMechanics if (weaptype == WeapType_HandToHand) { - static float fHandToHandReach = + static float fHandToHandReach = world->getStore().get().find("fHandToHandReach")->getFloat(); weapRange = fHandToHandReach; } - else if (weaptype != WeapType_PickProbe && weaptype != WeapType_Spell) + else if (weaptype != WeapType_PickProbe && weaptype != WeapType_Spell && weaptype != WeapType_None) { // All other WeapTypes are actually weapons, so get is safe. weapon = weaponSlot->get()->mBase; @@ -299,40 +355,49 @@ namespace MWMechanics } else //is creature { - weaptype = WeapType_HandToHand; //doesn't matter, should only reflect if it is melee or distant weapon + weaptype = actorClass.getCreatureStats(actor).getDrawState() == DrawState_Spell ? WeapType_Spell : WeapType_HandToHand; weapRange = 150.0f; //TODO: use true attack range (the same problem in Creature::hit) } - float rangeAttack; - float rangeFollow; bool distantCombat = false; - if (weaptype == WeapType_BowAndArrow || weaptype == WeapType_Crossbow || weaptype == WeapType_Thrown) + if (weaptype != WeapType_Spell) { - rangeAttack = 1000; // TODO: should depend on archer skill - rangeFollow = 0; // not needed in ranged combat - distantCombat = true; + // TODO: move to ActionWeapon + if (weaptype == WeapType_BowAndArrow || weaptype == WeapType_Crossbow || weaptype == WeapType_Thrown) + { + rangeAttack = 1000; + rangeFollow = 0; // not needed in ranged combat + distantCombat = true; + } + else + { + rangeAttack = weapRange; + rangeFollow = 300; + } } else { - rangeAttack = weapRange; - rangeFollow = 300; + distantCombat = (rangeAttack > 500); + weapRange = 150.f; } + + float& strength = storage.mStrength; // start new attack - if(mReadyToAttack) + if(readyToAttack) { - if(mTimerAttack <= -attacksPeriod) + if(timerAttack <= -attacksPeriod) { - mAttack = true; // attack starts just now + attack = true; // attack starts just now - if (!distantCombat) attackType = chooseBestAttack(weapon, mMovement); + if (!distantCombat) attackType = chooseBestAttack(weapon, movement); else attackType = ESM::Weapon::AT_Chop; // cause it's =0 - mStrength = static_cast(rand()) / RAND_MAX; + strength = static_cast(rand()) / RAND_MAX; // Note: may be 0 for some animations - mTimerAttack = mMinMaxAttackDuration[attackType][0] + - (mMinMaxAttackDuration[attackType][1] - mMinMaxAttackDuration[attackType][0]) * mStrength; + timerAttack = minMaxAttackDuration[attackType][0] + + (minMaxAttackDuration[attackType][1] - minMaxAttackDuration[attackType][0]) * strength; //say a provoking combat phrase if (actor.getClass().isNpc()) @@ -345,7 +410,7 @@ namespace MWMechanics MWBase::Environment::get().getDialogueManager()->say(actor, "attack"); } } - } + } } @@ -383,13 +448,16 @@ namespace MWMechanics Ogre::Vector3 vTargetPos(target.getRefData().getPosition().pos); Ogre::Vector3 vDirToTarget = vTargetPos - vActorPos; float distToTarget = vDirToTarget.length(); + + Ogre::Vector3& lastActorPos = storage.mLastActorPos; + bool& followTarget = storage.mFollowTarget; bool isStuck = false; float speed = 0.0f; - if(mMovement.mPosition[1] && (mLastActorPos - vActorPos).length() < (speed = actorClass.getSpeed(actor)) * tReaction / 2) + if(movement.mPosition[1] && (lastActorPos - vActorPos).length() < (speed = actorClass.getSpeed(actor)) * tReaction / 2) isStuck = true; - mLastActorPos = vActorPos; + lastActorPos = vActorPos; // check if actor can move along z-axis bool canMoveByZ = (actorClass.canSwim(actor) && world->isSwimming(actor)) @@ -398,8 +466,21 @@ namespace MWMechanics // for distant combat we should know if target is in LOS even if distToTarget < rangeAttack bool inLOS = distantCombat ? world->getLOS(actor, target) : true; + // can't fight if attacker can't go where target is. E.g. A fish can't attack person on land. + if (distToTarget >= rangeAttack + && !actorClass.isNpc() && !MWMechanics::isEnvironmentCompatible(actor, target)) + { + // TODO: start fleeing? + movement.mPosition[0] = 0; + movement.mPosition[1] = 0; + movement.mPosition[2] = 0; + readyToAttack = false; + actorClass.getCreatureStats(actor).setAttackingOrSpell(false); + return false; + } + // (within attack dist) || (not quite attack dist while following) - if(inLOS && (distToTarget < rangeAttack || (distToTarget <= rangeFollow && mFollowTarget && !isStuck))) + if(inLOS && (distToTarget < rangeAttack || (distToTarget <= rangeFollow && followTarget && !isStuck))) { //Melee and Close-up combat @@ -409,29 +490,30 @@ namespace MWMechanics // note: in getZAngleToDir if we preserve dir.z then horizontal angle can be inaccurate if (distantCombat) { - Ogre::Vector3 vAimDir = AimDirToMovingTarget(actor, target, mLastTargetPos, tReaction, weaptype, mStrength); - mLastTargetPos = vTargetPos; - mMovement.mRotation[0] = getXAngleToDir(vAimDir); - mMovement.mRotation[2] = getZAngleToDir(Ogre::Vector3(vAimDir.x, vAimDir.y, 0)); + Ogre::Vector3& lastTargetPos = storage.mLastTargetPos; + Ogre::Vector3 vAimDir = AimDirToMovingTarget(actor, target, lastTargetPos, tReaction, weaptype, strength); + lastTargetPos = vTargetPos; + movement.mRotation[0] = getXAngleToDir(vAimDir); + movement.mRotation[2] = getZAngleToDir(vAimDir); } else { - mMovement.mRotation[0] = getXAngleToDir(vDirToTarget, distToTarget); - mMovement.mRotation[2] = getZAngleToDir(Ogre::Vector3(vDirToTarget.x, vDirToTarget.y, 0)); + movement.mRotation[0] = getXAngleToDir(vDirToTarget, distToTarget); + movement.mRotation[2] = getZAngleToDir(vDirToTarget); } // (not quite attack dist while following) - if (mFollowTarget && distToTarget > rangeAttack) + if (followTarget && distToTarget > rangeAttack) { //Close-up combat: just run up on target - mMovement.mPosition[1] = 1; + movement.mPosition[1] = 1; } else // (within attack dist) { - if(mMovement.mPosition[0] || mMovement.mPosition[1]) + if(movement.mPosition[0] || movement.mPosition[1]) { - mTimerCombatMove = 0.1f + 0.1f * static_cast(rand())/RAND_MAX; - mCombatMove = true; + timerCombatMove = 0.1f + 0.1f * static_cast(rand())/RAND_MAX; + combatMove = true; } // only NPCs are smart enough to use dodge movements else if(actorClass.isNpc() && (!distantCombat || (distantCombat && distToTarget < rangeAttack/2))) @@ -439,20 +521,20 @@ namespace MWMechanics //apply sideway movement (kind of dodging) with some probability if(static_cast(rand())/RAND_MAX < 0.25) { - mMovement.mPosition[0] = static_cast(rand())/RAND_MAX < 0.5? 1: -1; - mTimerCombatMove = 0.05f + 0.15f * static_cast(rand())/RAND_MAX; - mCombatMove = true; + movement.mPosition[0] = static_cast(rand())/RAND_MAX < 0.5? 1: -1; + timerCombatMove = 0.05f + 0.15f * static_cast(rand())/RAND_MAX; + combatMove = true; } } if(distantCombat && distToTarget < rangeAttack/4) { - mMovement.mPosition[1] = -1; + movement.mPosition[1] = -1; } - mReadyToAttack = true; + readyToAttack = true; //only once got in melee combat, actor is allowed to use close-up shortcutting - mFollowTarget = true; + followTarget = true; } } else // remote pathfinding @@ -461,8 +543,11 @@ namespace MWMechanics if (!distantCombat) inLOS = world->getLOS(actor, target); // check if shortcut is available - if(inLOS && (!isStuck || mReadyToAttack) - && (!mForceNoShortcut || (Ogre::Vector3(mShortcutFailPos.pos) - vActorPos).length() >= PATHFIND_SHORTCUT_RETRY_DIST)) + bool& forceNoShortcut = storage.mForceNoShortcut; + ESM::Position& shortcutFailPos = storage.mShortcutFailPos; + + if(inLOS && (!isStuck || readyToAttack) + && (!forceNoShortcut || (Ogre::Vector3(shortcutFailPos.pos) - vActorPos).length() >= PATHFIND_SHORTCUT_RETRY_DIST)) { if(speed == 0.0f) speed = actorClass.getSpeed(actor); // maximum dist before pit/obstacle for actor to avoid them depending on his speed @@ -476,26 +561,26 @@ namespace MWMechanics if(preferShortcut) { if (canMoveByZ) - mMovement.mRotation[0] = getXAngleToDir(vDirToTarget, distToTarget); - mMovement.mRotation[2] = getZAngleToDir(vDirToTarget, distToTarget); - mForceNoShortcut = false; - mShortcutFailPos.pos[0] = mShortcutFailPos.pos[1] = mShortcutFailPos.pos[2] = 0; + movement.mRotation[0] = getXAngleToDir(vDirToTarget, distToTarget); + movement.mRotation[2] = getZAngleToDir(vDirToTarget); + forceNoShortcut = false; + shortcutFailPos.pos[0] = shortcutFailPos.pos[1] = shortcutFailPos.pos[2] = 0; mPathFinder.clearPath(); } else // if shortcut failed stick to path grid { - if(!isStuck && mShortcutFailPos.pos[0] == 0.0f && mShortcutFailPos.pos[1] == 0.0f && mShortcutFailPos.pos[2] == 0.0f) + if(!isStuck && shortcutFailPos.pos[0] == 0.0f && shortcutFailPos.pos[1] == 0.0f && shortcutFailPos.pos[2] == 0.0f) { - mForceNoShortcut = true; - mShortcutFailPos = pos; + forceNoShortcut = true; + shortcutFailPos = pos; } - mFollowTarget = false; + followTarget = false; buildNewPath(actor, target); //may fail to build a path, check before use //delete visited path node - mPathFinder.checkWaypoint(pos.pos[0],pos.pos[1],pos.pos[2]); + mPathFinder.checkPathCompleted(pos.pos[0],pos.pos[1],pos.pos[2]); // This works on the borders between the path grid and areas with no waypoints. if(inLOS && mPathFinder.getPath().size() > 1) @@ -508,7 +593,7 @@ namespace MWMechanics // if current actor pos is closer to target then last point of path (excluding target itself) then go straight on target if(distToTarget <= (vTargetPos - vBeforeTarget).length()) { - mMovement.mRotation[2] = getZAngleToDir(vDirToTarget, distToTarget); + movement.mRotation[2] = getZAngleToDir(vDirToTarget); preferShortcut = true; } } @@ -517,20 +602,20 @@ namespace MWMechanics if(!preferShortcut) { if(!mPathFinder.getPath().empty()) - mMovement.mRotation[2] = mPathFinder.getZAngleToNext(pos.pos[0], pos.pos[1]); + movement.mRotation[2] = mPathFinder.getZAngleToNext(pos.pos[0], pos.pos[1]); else - mMovement.mRotation[2] = getZAngleToDir(vDirToTarget, distToTarget); + movement.mRotation[2] = getZAngleToDir(vDirToTarget); } } - mMovement.mPosition[1] = 1; - if (mReadyToAttack) + movement.mPosition[1] = 1; + if (readyToAttack) { // to stop possible sideway moving after target moved out of attack range - mCombatMove = true; - mTimerCombatMove = 0; + combatMove = true; + timerCombatMove = 0; } - mReadyToAttack = false; + readyToAttack = false; } if(!isStuck && distToTarget > rangeAttack && !distantCombat) @@ -551,16 +636,16 @@ namespace MWMechanics float t = s1/speed1; float s2 = speed2 * t; float t_swing = - mMinMaxAttackDuration[ESM::Weapon::AT_Thrust][0] + - (mMinMaxAttackDuration[ESM::Weapon::AT_Thrust][1] - mMinMaxAttackDuration[ESM::Weapon::AT_Thrust][0]) * static_cast(rand()) / RAND_MAX; + minMaxAttackDuration[ESM::Weapon::AT_Thrust][0] + + (minMaxAttackDuration[ESM::Weapon::AT_Thrust][1] - minMaxAttackDuration[ESM::Weapon::AT_Thrust][0]) * static_cast(rand()) / RAND_MAX; if (t + s2/speed1 <= t_swing) { - mReadyToAttack = true; - if(mTimerAttack <= -attacksPeriod) + readyToAttack = true; + if(timerAttack <= -attacksPeriod) { - mTimerAttack = t_swing; - mAttack = true; + timerAttack = t_swing; + attack = true; } } } @@ -570,7 +655,7 @@ namespace MWMechanics // coded at 250ms or 1/4 second // // TODO: Add a parameter to vary DURATION_SAME_SPOT? - if((distToTarget > rangeAttack || mFollowTarget) && + if((distToTarget > rangeAttack || followTarget) && mObstacleCheck.check(actor, tReaction)) // check if evasive action needed { // probably walking into another NPC TODO: untested in combat situation @@ -582,8 +667,8 @@ namespace MWMechanics if(mPathFinder.isPathConstructed()) zTurn(actor, Ogre::Degree(mPathFinder.getZAngleToNext(pos.pos[0] + 1, pos.pos[1]))); - if(mFollowTarget) - mFollowTarget = false; + if(followTarget) + followTarget = false; // FIXME: can fool actors to stay behind doors, etc. // Related to Bug#1102 and to some degree #1155 as well } @@ -744,13 +829,15 @@ void getMinMaxAttackDuration(const MWWorld::Ptr& actor, float (*fMinMaxDurations // get weapon information: type and speed const ESM::Weapon *weapon = NULL; - MWMechanics::WeaponType weaptype; + MWMechanics::WeaponType weaptype = MWMechanics::WeapType_None; MWWorld::ContainerStoreIterator weaponSlot = MWMechanics::getActiveWeapon(actor.getClass().getCreatureStats(actor), actor.getClass().getInventoryStore(actor), &weaptype); float weapSpeed; - if (weaptype != MWMechanics::WeapType_HandToHand) + if (weaptype != MWMechanics::WeapType_HandToHand + && weaptype != MWMechanics::WeapType_Spell + && weaptype != MWMechanics::WeapType_None) { weapon = weaponSlot->get()->mBase; weapSpeed = weapon->mData.mSpeed; diff --git a/apps/openmw/mwmechanics/aicombat.hpp b/apps/openmw/mwmechanics/aicombat.hpp index 311dee617..307df3872 100644 --- a/apps/openmw/mwmechanics/aicombat.hpp +++ b/apps/openmw/mwmechanics/aicombat.hpp @@ -14,6 +14,8 @@ #include "../mwbase/world.hpp" +#include + namespace ESM { namespace AiSequence @@ -24,6 +26,8 @@ namespace ESM namespace MWMechanics { + class Action; + /// \brief Causes the actor to fight another actor class AiCombat : public AiPackage { @@ -38,7 +42,7 @@ namespace MWMechanics virtual AiCombat *clone() const; - virtual bool execute (const MWWorld::Ptr& actor,float duration); + virtual bool execute (const MWWorld::Ptr& actor, AiState& state, float duration); virtual int getTypeId() const; @@ -50,37 +54,14 @@ namespace MWMechanics virtual void writeState(ESM::AiSequence::AiSequence &sequence) const; private: - PathFinder mPathFinder; - // controls duration of the actual strike - float mTimerAttack; - float mTimerReact; - // controls duration of the sideway & forward moves - // when mCombatMove is true - float mTimerCombatMove; - - // AiCombat states - bool mReadyToAttack, mAttack; - bool mFollowTarget; - bool mCombatMove; - - float mStrength; // this is actually make sense only in ranged combat - float mMinMaxAttackDuration[3][2]; // slash, thrust, chop has different durations - bool mMinMaxAttackDurationInitialised; - - bool mForceNoShortcut; - ESM::Position mShortcutFailPos; - - Ogre::Vector3 mLastActorPos; - MWMechanics::Movement mMovement; int mTargetActorId; - Ogre::Vector3 mLastTargetPos; - const MWWorld::CellStore* mCell; - ObstacleCheck mObstacleCheck; void buildNewPath(const MWWorld::Ptr& actor, const MWWorld::Ptr& target); }; + + } #endif diff --git a/apps/openmw/mwmechanics/aicombataction.cpp b/apps/openmw/mwmechanics/aicombataction.cpp new file mode 100644 index 000000000..175b98001 --- /dev/null +++ b/apps/openmw/mwmechanics/aicombataction.cpp @@ -0,0 +1,560 @@ +#include "aicombataction.hpp" + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" + +#include "../mwworld/class.hpp" +#include "../mwworld/esmstore.hpp" +#include "../mwworld/inventorystore.hpp" +#include "../mwworld/actionequip.hpp" + +#include "../mwmechanics/npcstats.hpp" +#include "../mwmechanics/spellcasting.hpp" + +#include +#include + +namespace +{ + +// RangeTypes using bitflags to allow multiple range types, as can be the case with spells having multiple effects. +enum RangeTypes +{ + Self = 0x1, + Touch = 0x10, + Target = 0x100 +}; + +int getRangeTypes (const ESM::EffectList& effects) +{ + int types = 0; + for (std::vector::const_iterator it = effects.mList.begin(); it != effects.mList.end(); ++it) + { + if (it->mRange == ESM::RT_Self) + types |= Self; + else if (it->mRange == ESM::RT_Touch) + types |= Touch; + else if (it->mRange == ESM::RT_Target) + types |= Target; + } + return types; +} + +void suggestCombatRange(int rangeTypes, float& rangeAttack, float& rangeFollow) +{ + if (rangeTypes & Touch) + { + rangeAttack = 100.f; + rangeFollow = 300.f; + } + else if (rangeTypes & Target) + { + rangeAttack = 1000.f; + rangeFollow = 0.f; + } + else + { + // For Self spells, distance doesn't matter, so back away slightly to avoid enemy hits + rangeAttack = 600.f; + rangeFollow = 0.f; + } +} + +int numEffectsToCure (const MWWorld::Ptr& actor, int effectFilter=-1) +{ + int toCure=0; + const MWMechanics::ActiveSpells& activeSpells = actor.getClass().getCreatureStats(actor).getActiveSpells(); + for (MWMechanics::ActiveSpells::TIterator it = activeSpells.begin(); it != activeSpells.end(); ++it) + { + const MWMechanics::ActiveSpells::ActiveSpellParams& params = it->second; + for (std::vector::const_iterator effectIt = params.mEffects.begin(); + effectIt != params.mEffects.end(); ++effectIt) + { + int effectId = effectIt->mEffectId; + if (effectFilter != -1 && effectId != effectFilter) + continue; + const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find(effectId); + if (magicEffect->mData.mFlags & ESM::MagicEffect::Harmful + && effectIt->mDuration > 3 // Don't attempt to cure if effect runs out shortly anyway + ) + ++toCure; + } + } + return toCure; +} + +} + +namespace MWMechanics +{ + + float ratePotion (const MWWorld::Ptr &item, const MWWorld::Ptr& actor) + { + if (item.getTypeName() != typeid(ESM::Potion).name()) + return 0.f; + + const ESM::Potion* potion = item.get()->mBase; + return rateEffects(potion->mEffects, actor, MWWorld::Ptr()); + } + + float rateWeapon (const MWWorld::Ptr &item, const MWWorld::Ptr& actor, const MWWorld::Ptr& target, int type, + float arrowRating, float boltRating) + { + if (item.getTypeName() != typeid(ESM::Weapon).name()) + return 0.f; + + const ESM::Weapon* weapon = item.get()->mBase; + + if (type != -1 && weapon->mData.mType != type) + return 0.f; + + float rating=0.f; + + if (weapon->mData.mType >= ESM::Weapon::MarksmanBow) + { + rating = (weapon->mData.mChop[0] + weapon->mData.mChop[1]) / 2.f; + } + else + { + for (int i=0; i<2; ++i) + { + rating += weapon->mData.mSlash[i]; + rating += weapon->mData.mThrust[i]; + rating += weapon->mData.mChop[i]; + } + rating /= 6.f; + } + + if (item.getClass().hasItemHealth(item)) + { + if (item.getClass().getItemHealth(item) == 0) + return 0.f; + rating *= item.getClass().getItemHealth(item) / float(item.getClass().getItemMaxHealth(item)); + } + + if (weapon->mData.mType == ESM::Weapon::MarksmanBow) + { + if (arrowRating <= 0.f) + rating = 0.f; + else + rating += arrowRating; + } + else if (weapon->mData.mType == ESM::Weapon::MarksmanCrossbow) + { + if (boltRating <= 0.f) + rating = 0.f; + else + rating += boltRating; + } + + if (!weapon->mEnchant.empty()) + { + const ESM::Enchantment* enchantment = MWBase::Environment::get().getWorld()->getStore().get().find(weapon->mEnchant); + if (enchantment->mData.mType == ESM::Enchantment::WhenStrikes + && (item.getCellRef().getEnchantmentCharge() == -1 + || item.getCellRef().getEnchantmentCharge() >= enchantment->mData.mCost)) + rating += rateEffects(enchantment->mEffects, actor, target); + } + + int skill = item.getClass().getEquipmentSkill(item); + if (skill != -1) + rating *= actor.getClass().getSkill(actor, skill) / 100.f; + + return rating; + } + + float rateSpell(const ESM::Spell *spell, const MWWorld::Ptr &actor, const MWWorld::Ptr& target) + { + const CreatureStats& stats = actor.getClass().getCreatureStats(actor); + + if (MWMechanics::getSpellSuccessChance(spell, actor) == 0) + return 0.f; + + if (spell->mData.mType != ESM::Spell::ST_Spell) + return 0.f; + + // Don't make use of racial bonus spells, like MW. Can be made optional later + if (actor.getClass().isNpc()) + { + std::string raceid = actor.get()->mBase->mRace; + const ESM::Race* race = MWBase::Environment::get().getWorld()->getStore().get().find(raceid); + if (race->mPowers.exists(spell->mId)) + return 0.f; + } + + if (spell->mData.mCost > stats.getMagicka().getCurrent()) + return 0.f; + + // Spells don't stack, so early out if the spell is still active on the target + int types = getRangeTypes(spell->mEffects); + if ((types & Self) && stats.getActiveSpells().isSpellActive(spell->mId)) + return 0.f; + if ( ((types & Touch) || (types & Target)) && target.getClass().getCreatureStats(target).getActiveSpells().isSpellActive(spell->mId)) + return 0.f; + + return rateEffects(spell->mEffects, actor, target); + } + + float rateMagicItem(const MWWorld::Ptr &ptr, const MWWorld::Ptr &actor, const MWWorld::Ptr& target) + { + if (ptr.getClass().getEnchantment(ptr).empty()) + return 0.f; + + const ESM::Enchantment* enchantment = MWBase::Environment::get().getWorld()->getStore().get().find(ptr.getClass().getEnchantment(ptr)); + + if (enchantment->mData.mType == ESM::Enchantment::CastOnce) + { + return rateEffects(enchantment->mEffects, actor, target); + } + else + { + //if (!ptr.getClass().canBeEquipped(ptr, actor)) + return 0.f; + } + } + + float rateEffect(const ESM::ENAMstruct &effect, const MWWorld::Ptr &actor, const MWWorld::Ptr &target) + { + // NOTE: target may be empty + + float rating = 1; + switch (effect.mEffectID) + { + case ESM::MagicEffect::Soultrap: + case ESM::MagicEffect::AlmsiviIntervention: + case ESM::MagicEffect::DivineIntervention: + case ESM::MagicEffect::CalmHumanoid: + case ESM::MagicEffect::CalmCreature: + case ESM::MagicEffect::FrenzyHumanoid: + case ESM::MagicEffect::FrenzyCreature: + case ESM::MagicEffect::DemoralizeHumanoid: + case ESM::MagicEffect::DemoralizeCreature: + case ESM::MagicEffect::RallyHumanoid: + case ESM::MagicEffect::RallyCreature: + case ESM::MagicEffect::Charm: + case ESM::MagicEffect::DetectAnimal: + case ESM::MagicEffect::DetectEnchantment: + case ESM::MagicEffect::DetectKey: + case ESM::MagicEffect::Telekinesis: + case ESM::MagicEffect::Mark: + case ESM::MagicEffect::Recall: + case ESM::MagicEffect::Jump: + case ESM::MagicEffect::WaterBreathing: + case ESM::MagicEffect::SwiftSwim: + case ESM::MagicEffect::WaterWalking: + case ESM::MagicEffect::SlowFall: + case ESM::MagicEffect::Light: + case ESM::MagicEffect::Lock: + case ESM::MagicEffect::Open: + case ESM::MagicEffect::TurnUndead: + case ESM::MagicEffect::WeaknessToCommonDisease: + case ESM::MagicEffect::WeaknessToBlightDisease: + case ESM::MagicEffect::WeaknessToCorprusDisease: + case ESM::MagicEffect::CureCommonDisease: + case ESM::MagicEffect::CureBlightDisease: + case ESM::MagicEffect::CureCorprusDisease: + case ESM::MagicEffect::Invisibility: + return 0.f; + case ESM::MagicEffect::Feather: + if (actor.getClass().getEncumbrance(actor) - actor.getClass().getCapacity(actor) >= 0) + return 100.f; + else + return 0.f; + case ESM::MagicEffect::Levitate: + return 0.f; // AI isn't designed to take advantage of this, and could be perceived as unfair anyway + case ESM::MagicEffect::BoundBoots: + case ESM::MagicEffect::BoundHelm: + if (actor.getClass().isNpc()) + { + // Beast races can't wear helmets or boots + std::string raceid = actor.get()->mBase->mRace; + const ESM::Race* race = MWBase::Environment::get().getWorld()->getStore().get().find(raceid); + if (race->mData.mFlags & ESM::Race::Beast) + return 0.f; + } + // Intended fall-through + // Creatures can not wear armor + case ESM::MagicEffect::BoundCuirass: + case ESM::MagicEffect::BoundGloves: + if (!actor.getClass().isNpc()) + return 0.f; + break; + + case ESM::MagicEffect::RestoreHealth: + case ESM::MagicEffect::RestoreMagicka: + case ESM::MagicEffect::RestoreFatigue: + if (effect.mRange == ESM::RT_Self) + { + int priority = 1; + if (effect.mEffectID == ESM::MagicEffect::RestoreHealth) + priority = 10; + const DynamicStat& current = actor.getClass().getCreatureStats(actor). + getDynamic(effect.mEffectID - ESM::MagicEffect::RestoreHealth); + float toHeal = (effect.mMagnMin + effect.mMagnMax)/2.f * effect.mDuration; + // Effect doesn't heal more than we need, *or* we are below 1/2 health + if (current.getModified() - current.getCurrent() > toHeal + || current.getCurrent() < current.getModified()*0.5) + { + return 10000.f * priority + - (toHeal - (current.getModified()-current.getCurrent())); // prefer the most fitting potion + } + else + return -10000.f * priority; // Save for later + } + break; + + // Prefer Cure effects over Dispel, because Dispel also removes positive effects + case ESM::MagicEffect::Dispel: + return 1000.f * numEffectsToCure(actor); + case ESM::MagicEffect::CureParalyzation: + return 1001.f * numEffectsToCure(actor, ESM::MagicEffect::Paralyze); + case ESM::MagicEffect::CurePoison: + return 1001.f * numEffectsToCure(actor, ESM::MagicEffect::Poison); + + case ESM::MagicEffect::DisintegrateArmor: // TODO: check if actor is wearing armor + case ESM::MagicEffect::DisintegrateWeapon: // TODO: check if actor is wearing weapon + break; + + case ESM::MagicEffect::DamageAttribute: + case ESM::MagicEffect::DrainAttribute: + if (!target.isEmpty() && target.getClass().getCreatureStats(target).getAttribute(effect.mAttribute).getModified() <= 0) + return 0.f; + { + if (effect.mAttribute >= 0 && effect.mAttribute < ESM::Attribute::Length) + { + const float attributePriorities[ESM::Attribute::Length] = { + 1.f, // Strength + 0.5, // Intelligence + 0.6, // Willpower + 0.7, // Agility + 0.5, // Speed + 0.8, // Endurance + 0.7, // Personality + 0.3 // Luck + }; + rating *= attributePriorities[effect.mAttribute]; + } + } + break; + + case ESM::MagicEffect::DamageSkill: + case ESM::MagicEffect::DrainSkill: + if (target.isEmpty() || !target.getClass().isNpc()) + return 0.f; + if (target.getClass().getNpcStats(target).getSkill(effect.mSkill).getModified() <= 0) + return 0.f; + break; + + default: + break; + } + + // TODO: for non-cumulative effects (e.g. paralyze), check if the target is already suffering from them + + // TODO: could take into account target's resistance/weakness against the effect + + const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find(effect.mEffectID); + + rating *= magicEffect->mData.mBaseCost; + + if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude)) + rating *= (effect.mMagnMin + effect.mMagnMax)/2.f; + if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration)) + rating *= effect.mDuration; + + if (magicEffect->mData.mFlags & ESM::MagicEffect::Harmful) + rating *= -1.f; + + // Currently treating all "on target" or "on touch" effects to target the enemy actor. + // Combat AI is egoistic, so doesn't consider applying positive effects to friendly actors. + if (effect.mRange != ESM::RT_Self) + rating *= -1.f; + return rating; + } + + float rateEffects(const ESM::EffectList &list, const MWWorld::Ptr& actor, const MWWorld::Ptr& target) + { + // NOTE: target may be empty + float rating = 0.f; + for (std::vector::const_iterator it = list.mList.begin(); it != list.mList.end(); ++it) + { + rating += rateEffect(*it, actor, target); + } + return rating; + } + + void ActionSpell::prepare(const MWWorld::Ptr &actor) + { + actor.getClass().getCreatureStats(actor).getSpells().setSelectedSpell(mSpellId); + actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Spell); + if (actor.getClass().hasInventoryStore(actor)) + { + MWWorld::InventoryStore& inv = actor.getClass().getInventoryStore(actor); + inv.setSelectedEnchantItem(inv.end()); + } + } + + void ActionSpell::getCombatRange(float& rangeAttack, float& rangeFollow) + { + const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().find(mSpellId); + int types = getRangeTypes(spell->mEffects); + suggestCombatRange(types, rangeAttack, rangeFollow); + } + + void ActionEnchantedItem::prepare(const MWWorld::Ptr &actor) + { + actor.getClass().getCreatureStats(actor).getSpells().setSelectedSpell(std::string()); + actor.getClass().getInventoryStore(actor).setSelectedEnchantItem(mItem); + actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Spell); + } + + void ActionEnchantedItem::getCombatRange(float& rangeAttack, float& rangeFollow) + { + const ESM::Enchantment* enchantment = MWBase::Environment::get().getWorld()->getStore().get().find(mItem->getClass().getEnchantment(*mItem)); + int types = getRangeTypes(enchantment->mEffects); + suggestCombatRange(types, rangeAttack, rangeFollow); + } + + void ActionPotion::getCombatRange(float& rangeAttack, float& rangeFollow) + { + // distance doesn't matter, so back away slightly to avoid enemy hits + rangeAttack = 600.f; + rangeFollow = 0.f; + } + + void ActionPotion::prepare(const MWWorld::Ptr &actor) + { + actor.getClass().apply(actor, mPotion.getCellRef().getRefId(), actor); + actor.getClass().getContainerStore(actor).remove(mPotion, 1, actor); + } + + void ActionWeapon::prepare(const MWWorld::Ptr &actor) + { + if (actor.getClass().hasInventoryStore(actor)) + { + if (mWeapon.isEmpty()) + actor.getClass().getInventoryStore(actor).unequipSlot(MWWorld::InventoryStore::Slot_CarriedRight, actor); + else + { + MWWorld::ActionEquip equip(mWeapon); + equip.execute(actor); + } + + if (!mAmmunition.isEmpty()) + { + MWWorld::ActionEquip equip(mAmmunition); + equip.execute(actor); + } + } + actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Weapon); + } + + void ActionWeapon::getCombatRange(float& rangeAttack, float& rangeFollow) + { + // Already done in AiCombat itself + } + + boost::shared_ptr prepareNextAction(const MWWorld::Ptr &actor, const MWWorld::Ptr &target) + { + Spells& spells = actor.getClass().getCreatureStats(actor).getSpells(); + + float bestActionRating = 0.f; + // Default to hand-to-hand combat + boost::shared_ptr bestAction (new ActionWeapon(MWWorld::Ptr())); + if (actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) + { + bestAction->prepare(actor); + return bestAction; + } + + if (actor.getClass().hasInventoryStore(actor)) + { + MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); + + for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) + { + float rating = ratePotion(*it, actor); + if (rating > bestActionRating) + { + bestActionRating = rating; + bestAction.reset(new ActionPotion(*it)); + } + } + + for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) + { + float rating = rateMagicItem(*it, actor, target); + if (rating > bestActionRating) + { + bestActionRating = rating; + bestAction.reset(new ActionEnchantedItem(it)); + } + } + + float bestArrowRating = 0; + MWWorld::Ptr bestArrow; + for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) + { + float rating = rateWeapon(*it, actor, target, ESM::Weapon::Arrow); + if (rating > bestArrowRating) + { + bestArrowRating = rating; + bestArrow = *it; + } + } + + float bestBoltRating = 0; + MWWorld::Ptr bestBolt; + for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) + { + float rating = rateWeapon(*it, actor, target, ESM::Weapon::Bolt); + if (rating > bestBoltRating) + { + bestBoltRating = rating; + bestBolt = *it; + } + } + + for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) + { + std::vector equipmentSlots = it->getClass().getEquipmentSlots(*it).first; + if (std::find(equipmentSlots.begin(), equipmentSlots.end(), (int)MWWorld::InventoryStore::Slot_CarriedRight) + == equipmentSlots.end()) + continue; + + float rating = rateWeapon(*it, actor, target, -1, bestArrowRating, bestBoltRating); + if (rating > bestActionRating) + { + const ESM::Weapon* weapon = it->get()->mBase; + + MWWorld::Ptr ammo; + if (weapon->mData.mType == ESM::Weapon::MarksmanBow) + ammo = bestArrow; + else if (weapon->mData.mType == ESM::Weapon::MarksmanCrossbow) + ammo = bestBolt; + + bestActionRating = rating; + bestAction.reset(new ActionWeapon(*it, ammo)); + } + } + } + + for (Spells::TIterator it = spells.begin(); it != spells.end(); ++it) + { + const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().find(it->first); + + float rating = rateSpell(spell, actor, target); + if (rating > bestActionRating) + { + bestActionRating = rating; + bestAction.reset(new ActionSpell(spell->mId)); + } + } + + if (bestAction.get()) + bestAction->prepare(actor); + + return bestAction; + } + +} diff --git a/apps/openmw/mwmechanics/aicombataction.hpp b/apps/openmw/mwmechanics/aicombataction.hpp new file mode 100644 index 000000000..1c7451c32 --- /dev/null +++ b/apps/openmw/mwmechanics/aicombataction.hpp @@ -0,0 +1,90 @@ +#ifndef OPENMW_AICOMBAT_ACTION_H +#define OPENMW_AICOMBAT_ACTION_H + +#include + +#include "../mwworld/ptr.hpp" +#include "../mwworld/containerstore.hpp" + +#include + +namespace MWMechanics +{ + + class Action + { + public: + virtual ~Action() {} + virtual void prepare(const MWWorld::Ptr& actor) = 0; + virtual void getCombatRange (float& rangeAttack, float& rangeFollow) = 0; + virtual float getActionCooldown() { return 0.f; } + }; + + class ActionSpell : public Action + { + public: + ActionSpell(const std::string& spellId) : mSpellId(spellId) {} + std::string mSpellId; + /// Sets the given spell as selected on the actor's spell list. + virtual void prepare(const MWWorld::Ptr& actor); + + virtual void getCombatRange (float& rangeAttack, float& rangeFollow); + }; + + class ActionEnchantedItem : public Action + { + public: + ActionEnchantedItem(const MWWorld::ContainerStoreIterator& item) : mItem(item) {} + MWWorld::ContainerStoreIterator mItem; + /// Sets the given item as selected enchanted item in the actor's InventoryStore. + virtual void prepare(const MWWorld::Ptr& actor); + virtual void getCombatRange (float& rangeAttack, float& rangeFollow); + + /// Since this action has no animation, apply a small cool down for using it + virtual float getActionCooldown() { return 1.f; } + }; + + class ActionPotion : public Action + { + public: + ActionPotion(const MWWorld::Ptr& potion) : mPotion(potion) {} + MWWorld::Ptr mPotion; + /// Drinks the given potion. + virtual void prepare(const MWWorld::Ptr& actor); + virtual void getCombatRange (float& rangeAttack, float& rangeFollow); + + /// Since this action has no animation, apply a small cool down for using it + virtual float getActionCooldown() { return 1.f; } + }; + + class ActionWeapon : public Action + { + private: + MWWorld::Ptr mAmmunition; + MWWorld::Ptr mWeapon; + + public: + /// \a weapon may be empty for hand-to-hand combat + ActionWeapon(const MWWorld::Ptr& weapon, const MWWorld::Ptr& ammo = MWWorld::Ptr()) + : mWeapon(weapon), mAmmunition(ammo) {} + /// Equips the given weapon. + virtual void prepare(const MWWorld::Ptr& actor); + virtual void getCombatRange (float& rangeAttack, float& rangeFollow); + }; + + float rateSpell (const ESM::Spell* spell, const MWWorld::Ptr& actor, const MWWorld::Ptr& target); + float rateMagicItem (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor, const MWWorld::Ptr &target); + float ratePotion (const MWWorld::Ptr& item, const MWWorld::Ptr &actor); + /// @param type Skip all weapons that are not of this type (i.e. return rating 0) + float rateWeapon (const MWWorld::Ptr& item, const MWWorld::Ptr& actor, const MWWorld::Ptr& target, + int type=-1, float arrowRating=0.f, float boltRating=0.f); + + /// @note target may be empty + float rateEffect (const ESM::ENAMstruct& effect, const MWWorld::Ptr& actor, const MWWorld::Ptr& target); + /// @note target may be empty + float rateEffects (const ESM::EffectList& list, const MWWorld::Ptr& actor, const MWWorld::Ptr& target); + + boost::shared_ptr prepareNextAction (const MWWorld::Ptr& actor, const MWWorld::Ptr& target); +} + +#endif diff --git a/apps/openmw/mwmechanics/aiescort.cpp b/apps/openmw/mwmechanics/aiescort.cpp index 3f5724077..c89cfe492 100644 --- a/apps/openmw/mwmechanics/aiescort.cpp +++ b/apps/openmw/mwmechanics/aiescort.cpp @@ -9,6 +9,8 @@ #include "../mwworld/cellstore.hpp" #include "../mwworld/class.hpp" +#include "../mwmechanics/creaturestats.hpp" + #include "steering.hpp" #include "movement.hpp" @@ -25,7 +27,7 @@ namespace MWMechanics , mCellX(std::numeric_limits::max()) , mCellY(std::numeric_limits::max()) { - mMaxDist = 470; + mMaxDist = 450; // The CS Help File states that if a duration is given, the AI package will run for that long // BUT if a location is givin, it "trumps" the duration so it will simply escort to that location. @@ -38,7 +40,7 @@ namespace MWMechanics , mCellX(std::numeric_limits::max()) , mCellY(std::numeric_limits::max()) { - mMaxDist = 470; + mMaxDist = 450; // The CS Help File states that if a duration is given, the AI package will run for that long // BUT if a location is given, it "trumps" the duration so it will simply escort to that location. @@ -52,6 +54,7 @@ namespace MWMechanics , mCellY(std::numeric_limits::max()) , mCellId(escort->mCellId) , mRemainingDuration(escort->mRemainingDuration) + , mMaxDist(450) { } @@ -61,7 +64,7 @@ namespace MWMechanics return new AiEscort(*this); } - bool AiEscort::execute (const MWWorld::Ptr& actor,float duration) + bool AiEscort::execute (const MWWorld::Ptr& actor, AiState& state, float duration) { // If AiEscort has ran for as long or longer then the duration specified // and the duration is not infinite, the package is complete. @@ -72,6 +75,9 @@ namespace MWMechanics return true; } + actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Nothing); + actor.getClass().getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, false); + const MWWorld::Ptr follower = MWBase::Environment::get().getWorld()->getPtr(mActorId, false); const float* const leaderPos = actor.getRefData().getPosition().pos; const float* const followerPos = follower.getRefData().getPosition().pos; @@ -86,16 +92,20 @@ namespace MWMechanics if(distanceBetweenResult <= mMaxDist * mMaxDist) { - if(pathTo(actor,ESM::Pathgrid::Point(mX,mY,mZ),duration)) //Returns true on path complete + ESM::Pathgrid::Point point(mX,mY,mZ); + point.mAutogenerated = 0; + point.mConnectionNum = 0; + point.mUnknown = 0; + if(pathTo(actor,point,duration)) //Returns true on path complete return true; - mMaxDist = 470; + mMaxDist = 450; } else { // Stop moving if the player is to far away MWBase::Environment::get().getMechanicsManager()->playAnimationGroup(actor, "idle3", 0, 1); actor.getClass().getMovementSettings(actor).mPosition[1] = 0; - mMaxDist = 330; + mMaxDist = 250; } return false; diff --git a/apps/openmw/mwmechanics/aiescort.hpp b/apps/openmw/mwmechanics/aiescort.hpp index 820df969f..f02cdba22 100644 --- a/apps/openmw/mwmechanics/aiescort.hpp +++ b/apps/openmw/mwmechanics/aiescort.hpp @@ -33,7 +33,7 @@ namespace MWMechanics virtual AiEscort *clone() const; - virtual bool execute (const MWWorld::Ptr& actor,float duration); + virtual bool execute (const MWWorld::Ptr& actor, AiState& state, float duration); virtual int getTypeId() const; @@ -48,7 +48,6 @@ namespace MWMechanics float mMaxDist; float mRemainingDuration; // In seconds - PathFinder mPathFinder; int mCellX; int mCellY; }; diff --git a/apps/openmw/mwmechanics/aifollow.cpp b/apps/openmw/mwmechanics/aifollow.cpp index 5ab7e1730..161f4bb90 100644 --- a/apps/openmw/mwmechanics/aifollow.cpp +++ b/apps/openmw/mwmechanics/aifollow.cpp @@ -6,37 +6,100 @@ #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" +#include "../mwbase/mechanicsmanager.hpp" #include "../mwworld/class.hpp" #include "../mwworld/cellstore.hpp" #include "creaturestats.hpp" #include "movement.hpp" #include +#include #include "steering.hpp" -MWMechanics::AiFollow::AiFollow(const std::string &actorId,float duration, float x, float y, float z) -: mAlwaysFollow(false), mRemainingDuration(duration), mX(x), mY(y), mZ(z), mActorId(actorId), mCellId("") +namespace MWMechanics +{ + + +struct AiFollowStorage : AiTemporaryBase +{ + float mTimer; + + AiFollowStorage() : mTimer(0.f) {} +}; + +int AiFollow::mFollowIndexCounter = 0; + +AiFollow::AiFollow(const std::string &actorId,float duration, float x, float y, float z) +: mAlwaysFollow(false), mCommanded(false), mRemainingDuration(duration), mX(x), mY(y), mZ(z) +, mActorRefId(actorId), mCellId(""), mActorId(-1), mFollowIndex(mFollowIndexCounter++), mActive(false) { } -MWMechanics::AiFollow::AiFollow(const std::string &actorId,const std::string &cellId,float duration, float x, float y, float z) -: mAlwaysFollow(false), mRemainingDuration(duration), mX(x), mY(y), mZ(z), mActorId(actorId), mCellId(cellId) +AiFollow::AiFollow(const std::string &actorId,const std::string &cellId,float duration, float x, float y, float z) +: mAlwaysFollow(false), mCommanded(false), mRemainingDuration(duration), mX(x), mY(y), mZ(z) +, mActorRefId(actorId), mCellId(cellId), mActorId(-1), mFollowIndex(mFollowIndexCounter++), mActive(false) { } -MWMechanics::AiFollow::AiFollow(const std::string &actorId) -: mAlwaysFollow(true), mRemainingDuration(0), mX(0), mY(0), mZ(0), mActorId(actorId), mCellId("") +AiFollow::AiFollow(const std::string &actorId, bool commanded) +: mAlwaysFollow(true), mCommanded(commanded), mRemainingDuration(0), mX(0), mY(0), mZ(0) +, mActorRefId(actorId), mCellId(""), mActorId(-1), mFollowIndex(mFollowIndexCounter++), mActive(false) +{ + +} + +AiFollow::AiFollow(const ESM::AiSequence::AiFollow *follow) + : mAlwaysFollow(follow->mAlwaysFollow), mRemainingDuration(follow->mRemainingDuration) + , mX(follow->mData.mX), mY(follow->mData.mY), mZ(follow->mData.mZ) + , mActorRefId(follow->mTargetId), mActorId(-1), mCellId(follow->mCellId) + , mCommanded(follow->mCommanded), mFollowIndex(mFollowIndexCounter++), mActive(follow->mActive) { + } -bool MWMechanics::AiFollow::execute (const MWWorld::Ptr& actor,float duration) +bool AiFollow::execute (const MWWorld::Ptr& actor, AiState& state, float duration) { - const MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtr(mActorId, false); //The target to follow + MWWorld::Ptr target = getTarget(); - if(target == MWWorld::Ptr()) return true; //Target doesn't exist + if (target.isEmpty() || !target.getRefData().getCount() || !target.getRefData().isEnabled() // Really we should be checking whether the target is currently registered + // with the MechanicsManager + ) + return false; // Target is not here right now, wait for it to return + + actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Nothing); + + // AiFollow requires the target to be in range and within sight for the initial activation + if (!mActive) + { + AiFollowStorage& storage = state.get(); + storage.mTimer -= duration; + + if (storage.mTimer < 0) + { + if (Ogre::Vector3(actor.getRefData().getPosition().pos).squaredDistance(Ogre::Vector3(target.getRefData().getPosition().pos)) + < 500*500 + && MWBase::Environment::get().getWorld()->getLOS(actor, target)) + mActive = true; + storage.mTimer = 0.5f; + } + } + if (!mActive) + return false; ESM::Position pos = actor.getRefData().getPosition(); //position of the actor + float followDistance = 180; + // When there are multiple actors following the same target, they form a group with each group member at 180*(i+1) distance to the target + int i=0; + std::list followers = MWBase::Environment::get().getMechanicsManager()->getActorsFollowingIndices(target); + followers.sort(); + for (std::list::iterator it = followers.begin(); it != followers.end(); ++it) + { + if (*it == mFollowIndex) + followDistance *= (i+1); + ++i; + } + if(!mAlwaysFollow) //Update if you only follow for a bit { //Check if we've run out of time @@ -49,7 +112,7 @@ bool MWMechanics::AiFollow::execute (const MWWorld::Ptr& actor,float duration) if((pos.pos[0]-mX)*(pos.pos[0]-mX) + (pos.pos[1]-mY)*(pos.pos[1]-mY) + - (pos.pos[2]-mZ)*(pos.pos[2]-mZ) < 100*100) //Close-ish to final position + (pos.pos[2]-mZ)*(pos.pos[2]-mZ) < followDistance*followDistance) //Close-ish to final position { if(actor.getCell()->isExterior()) //Outside? { @@ -67,46 +130,61 @@ bool MWMechanics::AiFollow::execute (const MWWorld::Ptr& actor,float duration) //Set the target destination from the actor ESM::Pathgrid::Point dest = target.getRefData().getPosition().pos; - if(distance(dest, pos.pos[0], pos.pos[1], pos.pos[2]) < 100) //Stop when you get close + if(distance(dest, pos.pos[0], pos.pos[1], pos.pos[2]) < followDistance) //Stop when you get close + { actor.getClass().getMovementSettings(actor).mPosition[1] = 0; - else { + + // turn towards target anyway + float directionX = target.getRefData().getPosition().pos[0] - actor.getRefData().getPosition().pos[0]; + float directionY = target.getRefData().getPosition().pos[1] - actor.getRefData().getPosition().pos[1]; + zTurn(actor, Ogre::Math::ATan2(directionX,directionY), Ogre::Degree(5)); + } + else + { pathTo(actor, dest, duration); //Go to the destination } //Check if you're far away - if(distance(dest, pos.pos[0], pos.pos[1], pos.pos[2]) > 1000) + if(distance(dest, pos.pos[0], pos.pos[1], pos.pos[2]) > 450) actor.getClass().getCreatureStats(actor).setMovementFlag(MWMechanics::CreatureStats::Flag_Run, true); //Make NPC run - else if(distance(dest, pos.pos[0], pos.pos[1], pos.pos[2]) < 800) //Have a bit of a dead zone, otherwise npc will constantly flip between running and not when right on the edge of the running threshhold + else if(distance(dest, pos.pos[0], pos.pos[1], pos.pos[2]) < 325) //Have a bit of a dead zone, otherwise npc will constantly flip between running and not when right on the edge of the running threshhold actor.getClass().getCreatureStats(actor).setMovementFlag(MWMechanics::CreatureStats::Flag_Run, false); //make NPC walk return false; } -std::string MWMechanics::AiFollow::getFollowedActor() +std::string AiFollow::getFollowedActor() { - return mActorId; + return mActorRefId; } -MWMechanics::AiFollow *MWMechanics::AiFollow::clone() const +AiFollow *MWMechanics::AiFollow::clone() const { return new AiFollow(*this); } -int MWMechanics::AiFollow::getTypeId() const +int AiFollow::getTypeId() const { return TypeIdFollow; } -void MWMechanics::AiFollow::writeState(ESM::AiSequence::AiSequence &sequence) const +bool AiFollow::isCommanded() const +{ + return mCommanded; +} + +void AiFollow::writeState(ESM::AiSequence::AiSequence &sequence) const { std::auto_ptr follow(new ESM::AiSequence::AiFollow()); follow->mData.mX = mX; follow->mData.mY = mY; follow->mData.mZ = mZ; - follow->mTargetId = mActorId; + follow->mTargetId = mActorRefId; follow->mRemainingDuration = mRemainingDuration; follow->mCellId = mCellId; follow->mAlwaysFollow = mAlwaysFollow; + follow->mCommanded = mCommanded; + follow->mActive = mActive; ESM::AiSequence::AiPackageContainer package; package.mType = ESM::AiSequence::Ai_Follow; @@ -114,10 +192,32 @@ void MWMechanics::AiFollow::writeState(ESM::AiSequence::AiSequence &sequence) co sequence.mPackages.push_back(package); } -MWMechanics::AiFollow::AiFollow(const ESM::AiSequence::AiFollow *follow) - : mAlwaysFollow(follow->mAlwaysFollow), mRemainingDuration(follow->mRemainingDuration) - , mX(follow->mData.mX), mY(follow->mData.mY), mZ(follow->mData.mZ) - , mActorId(follow->mTargetId), mCellId(follow->mCellId) +MWWorld::Ptr AiFollow::getTarget() { + if (mActorId == -2) + return MWWorld::Ptr(); + + if (mActorId == -1) + { + MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtr(mActorRefId, false); + if (target.isEmpty()) + { + mActorId = -2; + return target; + } + else + mActorId = target.getClass().getCreatureStats(target).getActorId(); + } + + if (mActorId != -1) + return MWBase::Environment::get().getWorld()->searchPtrViaActorId(mActorId); + else + return MWWorld::Ptr(); +} + +int AiFollow::getFollowIndex() const +{ + return mFollowIndex; +} } diff --git a/apps/openmw/mwmechanics/aifollow.hpp b/apps/openmw/mwmechanics/aifollow.hpp index e9587b36e..68a1f0ea5 100644 --- a/apps/openmw/mwmechanics/aifollow.hpp +++ b/apps/openmw/mwmechanics/aifollow.hpp @@ -27,13 +27,15 @@ namespace MWMechanics /// Follow Actor for duration or until you arrive at a position in a cell AiFollow(const std::string &ActorId,const std::string &CellId,float duration, float X, float Y, float Z); /// Follow Actor indefinitively - AiFollow(const std::string &ActorId); + AiFollow(const std::string &ActorId, bool commanded=false); AiFollow(const ESM::AiSequence::AiFollow* follow); + MWWorld::Ptr getTarget(); + virtual AiFollow *clone() const; - virtual bool execute (const MWWorld::Ptr& actor,float duration); + virtual bool execute (const MWWorld::Ptr& actor, AiState& state, float duration); virtual int getTypeId() const; @@ -42,16 +44,26 @@ namespace MWMechanics virtual void writeState (ESM::AiSequence::AiSequence& sequence) const; + bool isCommanded() const; + + int getFollowIndex() const; + private: /// This will make the actor always follow. /** Thus ignoring mDuration and mX,mY,mZ (used for summoned creatures). **/ bool mAlwaysFollow; + bool mCommanded; float mRemainingDuration; // Seconds float mX; float mY; float mZ; - std::string mActorId; + std::string mActorRefId; + int mActorId; std::string mCellId; + bool mActive; // have we spotted the target? + int mFollowIndex; + + static int mFollowIndexCounter; }; } #endif diff --git a/apps/openmw/mwmechanics/aipackage.cpp b/apps/openmw/mwmechanics/aipackage.cpp index 7790942b2..f015bb8a4 100644 --- a/apps/openmw/mwmechanics/aipackage.cpp +++ b/apps/openmw/mwmechanics/aipackage.cpp @@ -16,7 +16,7 @@ MWMechanics::AiPackage::~AiPackage() {} -MWMechanics::AiPackage::AiPackage() : mLastDoorChecked(MWWorld::Ptr()), mTimer(.26), mStuckTimer(0) { //mTimer starts at .26 to force initial pathbuild +MWMechanics::AiPackage::AiPackage() : mTimer(.26), mStuckTimer(0) { //mTimer starts at .26 to force initial pathbuild } @@ -92,22 +92,19 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, ESM::Pathgrid::Po { /// TODO (tluppi#1#): Use ObstacleCheck here. Not working for some reason //if(mObstacleCheck.check(actor, duration)) { - if(distance(start, mStuckPos.pos[0], mStuckPos.pos[1], mStuckPos.pos[2]) < 10 && distance(dest, start) > 20) { //Actually stuck, and far enough away from destination to care + if(distance(start, mStuckPos.pos[0], mStuckPos.pos[1], mStuckPos.pos[2]) < actor.getClass().getSpeed(actor)*0.05 && distance(dest, start) > 20) { //Actually stuck, and far enough away from destination to care // first check if we're walking into a door MWWorld::Ptr door = getNearbyDoor(actor); if(door != MWWorld::Ptr()) // NOTE: checks interior cells only { - if(door.getCellRef().getTrap().empty() && mLastDoorChecked != door) { //Open the door if untrapped - door.getClass().activate(door, actor).get()->execute(actor); - mLastDoorChecked = door; + if(!door.getCellRef().getTeleport() && door.getCellRef().getTrap().empty() && door.getClass().getDoorState(door) == 0) { //Open the door if untrapped + MWBase::Environment::get().getWorld()->activateDoor(door, 1); } } else // probably walking into another NPC { - // TODO: diagonal should have same animation as walk forward - // but doesn't seem to do that? actor.getClass().getMovementSettings(actor).mPosition[0] = 1; - actor.getClass().getMovementSettings(actor).mPosition[1] = 0.1f; + actor.getClass().getMovementSettings(actor).mPosition[1] = 1; // change the angle a bit, too zTurn(actor, Ogre::Degree(mPathFinder.getZAngleToNext(pos.pos[0] + 1, pos.pos[1]))); } @@ -115,7 +112,6 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, ESM::Pathgrid::Po else { //Not stuck, so reset things mStuckTimer = 0; mStuckPos = pos; - mLastDoorChecked = MWWorld::Ptr(); //Resets it, in case he gets stuck behind the door again actor.getClass().getMovementSettings(actor).mPosition[1] = 1; //Just run forward } } diff --git a/apps/openmw/mwmechanics/aipackage.hpp b/apps/openmw/mwmechanics/aipackage.hpp index 983777c0a..80b48fc37 100644 --- a/apps/openmw/mwmechanics/aipackage.hpp +++ b/apps/openmw/mwmechanics/aipackage.hpp @@ -3,9 +3,9 @@ #include "pathfinding.hpp" #include -#include "../mwbase/world.hpp" #include "obstacle.hpp" +#include "aistate.hpp" namespace MWWorld { @@ -20,8 +20,10 @@ namespace ESM } } + namespace MWMechanics { + /// \brief Base class for AI packages class AiPackage { @@ -50,7 +52,7 @@ namespace MWMechanics /// Updates and runs the package (Should run every frame) /// \return Package completed? - virtual bool execute (const MWWorld::Ptr& actor,float duration) = 0; + virtual bool execute (const MWWorld::Ptr& actor, AiState& state, float duration) = 0; /// Returns the TypeID of the AiPackage /// \see enum TypeId @@ -61,20 +63,21 @@ namespace MWMechanics virtual void writeState (ESM::AiSequence::AiSequence& sequence) const {} + /// Simulates the passing of time + virtual void fastForward(const MWWorld::Ptr& actor, AiState& state) {} + protected: /// Causes the actor to attempt to walk to the specified location /** \return If the actor has arrived at his destination **/ bool pathTo(const MWWorld::Ptr& actor, ESM::Pathgrid::Point dest, float duration); + // TODO: all this does not belong here, move into temporary storage PathFinder mPathFinder; ObstacleCheck mObstacleCheck; - float mDoorCheckDuration; float mTimer; float mStuckTimer; - MWWorld::Ptr mLastDoorChecked; //Used to ensure we don't try to CONSTANTLY open a door - ESM::Position mStuckPos; ESM::Pathgrid::Point mPrevDest; }; diff --git a/apps/openmw/mwmechanics/aipursue.cpp b/apps/openmw/mwmechanics/aipursue.cpp index 60f671c12..8c31a10db 100644 --- a/apps/openmw/mwmechanics/aipursue.cpp +++ b/apps/openmw/mwmechanics/aipursue.cpp @@ -30,14 +30,28 @@ AiPursue *MWMechanics::AiPursue::clone() const { return new AiPursue(*this); } -bool AiPursue::execute (const MWWorld::Ptr& actor, float duration) +bool AiPursue::execute (const MWWorld::Ptr& actor, AiState& state, float duration) { + if(actor.getClass().getCreatureStats(actor).isDead()) + return true; + ESM::Position pos = actor.getRefData().getPosition(); //position of the actor const MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtrViaActorId(mTargetActorId); //The target to follow - if(target == MWWorld::Ptr()) + if(target == MWWorld::Ptr() || !target.getRefData().getCount() || !target.getRefData().isEnabled() // Really we should be checking whether the target is currently registered + // with the MechanicsManager + ) return true; //Target doesn't exist + if (target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::Invisibility).getMagnitude() > 0 + || target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::Chameleon).getMagnitude() > 75) + return true; + + if(target.getClass().getCreatureStats(target).isDead()) + return true; + + actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Nothing); + //Set the target desition from the actor ESM::Pathgrid::Point dest = target.getRefData().getPosition().pos; diff --git a/apps/openmw/mwmechanics/aipursue.hpp b/apps/openmw/mwmechanics/aipursue.hpp index 18a22b676..493a27985 100644 --- a/apps/openmw/mwmechanics/aipursue.hpp +++ b/apps/openmw/mwmechanics/aipursue.hpp @@ -31,7 +31,7 @@ namespace MWMechanics AiPursue(const ESM::AiSequence::AiPursue* pursue); virtual AiPursue *clone() const; - virtual bool execute (const MWWorld::Ptr& actor,float duration); + virtual bool execute (const MWWorld::Ptr& actor, AiState& state, float duration); virtual int getTypeId() const; MWWorld::Ptr getTarget() const; @@ -41,8 +41,6 @@ namespace MWMechanics private: int mTargetActorId; // The actor to pursue - int mCellX; - int mCellY; }; } #endif diff --git a/apps/openmw/mwmechanics/aisequence.cpp b/apps/openmw/mwmechanics/aisequence.cpp index 02f00dfc6..533bcd17c 100644 --- a/apps/openmw/mwmechanics/aisequence.cpp +++ b/apps/openmw/mwmechanics/aisequence.cpp @@ -2,6 +2,7 @@ #include "aisequence.hpp" #include "aipackage.hpp" +#include "aistate.hpp" #include "aiwander.hpp" #include "aiescort.hpp" @@ -31,9 +32,11 @@ void AiSequence::copy (const AiSequence& sequence) AiSequence::AiSequence() : mDone (false), mLastAiPackage(-1) {} -AiSequence::AiSequence (const AiSequence& sequence) : mDone (false) +AiSequence::AiSequence (const AiSequence& sequence) { copy (sequence); + mDone = sequence.mDone; + mLastAiPackage = sequence.mLastAiPackage; } AiSequence& AiSequence::operator= (const AiSequence& sequence) @@ -43,6 +46,7 @@ AiSequence& AiSequence::operator= (const AiSequence& sequence) clear(); copy (sequence); mDone = sequence.mDone; + mLastAiPackage = sequence.mLastAiPackage; } return *this; @@ -72,6 +76,29 @@ bool AiSequence::getCombatTarget(MWWorld::Ptr &targetActor) const return true; } +std::list::const_iterator AiSequence::begin() const +{ + return mPackages.begin(); +} + +std::list::const_iterator AiSequence::end() const +{ + return mPackages.end(); +} + +std::list::const_iterator AiSequence::erase(std::list::const_iterator package) +{ + // Not sure if manually terminated packages should trigger mDone, probably not? + for(std::list::iterator it = mPackages.begin(); it != mPackages.end(); ++it) + { + if (package == it) + { + return mPackages.erase(it); + } + } + throw std::runtime_error("can't find package to erase"); +} + bool AiSequence::isInCombat() const { for(std::list::const_iterator it = mPackages.begin(); it != mPackages.end(); ++it) @@ -96,48 +123,25 @@ bool AiSequence::isInCombat(const MWWorld::Ptr &actor) const return false; } -bool AiSequence::canAddTarget(const ESM::Position& actorPos, float distToTarget) const -{ - bool firstCombatFound = false; - MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); - - for(std::list::const_iterator it = mPackages.begin(); it != mPackages.end(); ++it) - { - if ((*it)->getTypeId() == AiPackage::TypeIdCombat) - { - firstCombatFound = true; - - const AiCombat *combat = static_cast(*it); - if (combat->getTarget() != player ) return false; // only 1 non-player target allowed - else - { - // add new target only if current target (player) is farther - const ESM::Position &targetPos = combat->getTarget().getRefData().getPosition(); - - float distToCurrTarget = (Ogre::Vector3(targetPos.pos) - Ogre::Vector3(actorPos.pos)).length(); - return (distToCurrTarget > distToTarget); - } - } - else if (firstCombatFound) break; // assumes combat packages go one-by-one in packages list - } - return true; -} - void AiSequence::stopCombat() { - while (getTypeId() == AiPackage::TypeIdCombat) + for(std::list::iterator it = mPackages.begin(); it != mPackages.end(); ) { - delete *mPackages.begin(); - mPackages.erase (mPackages.begin()); + if ((*it)->getTypeId() == AiPackage::TypeIdCombat) + it = mPackages.erase(it); + else + ++it; } } void AiSequence::stopPursuit() { - while (getTypeId() == AiPackage::TypeIdPursue) + for(std::list::iterator it = mPackages.begin(); it != mPackages.end(); ) { - delete *mPackages.begin(); - mPackages.erase (mPackages.begin()); + if ((*it)->getTypeId() == AiPackage::TypeIdPursue) + it = mPackages.erase(it); + else + ++it; } } @@ -146,7 +150,7 @@ bool AiSequence::isPackageDone() const return mDone; } -void AiSequence::execute (const MWWorld::Ptr& actor,float duration) +void AiSequence::execute (const MWWorld::Ptr& actor, AiState& state,float duration) { if(actor != MWBase::Environment::get().getWorld()->getPlayerPtr()) { @@ -207,7 +211,7 @@ void AiSequence::execute (const MWWorld::Ptr& actor,float duration) } } - if (package->execute (actor,duration)) + if (package->execute (actor,state,duration)) { // To account for the rare case where AiPackage::execute() queued another AI package // (e.g. AiPursue executing a dialogue script that uses startCombat) @@ -235,6 +239,9 @@ void AiSequence::clear() void AiSequence::stack (const AiPackage& package, const MWWorld::Ptr& actor) { + if (actor == MWBase::Environment::get().getWorld()->getPlayerPtr()) + throw std::runtime_error("Can't add AI packages to player"); + if (package.getTypeId() == AiPackage::TypeIdCombat || package.getTypeId() == AiPackage::TypeIdPursue) { // Notify AiWander of our current position so we can return to it after combat finished @@ -257,20 +264,14 @@ void AiSequence::stack (const AiPackage& package, const MWWorld::Ptr& actor) for(std::list::iterator it = mPackages.begin(); it != mPackages.end(); ++it) { - if(mPackages.front()->getPriority() <= package.getPriority()) + if((*it)->getPriority() <= package.getPriority()) { mPackages.insert(it,package.clone()); return; } } - if(mPackages.empty()) - mPackages.push_front (package.clone()); -} - -void AiSequence::queue (const AiPackage& package) -{ - mPackages.push_back (package.clone()); + mPackages.push_front (package.clone()); } AiPackage* MWMechanics::AiSequence::getActivePackage() @@ -340,49 +341,49 @@ void AiSequence::readState(const ESM::AiSequence::AiSequence &sequence) case ESM::AiSequence::Ai_Wander: { MWMechanics::AiWander* wander = new AiWander( - dynamic_cast(it->mPackage)); + static_cast(it->mPackage)); mPackages.push_back(wander); break; } case ESM::AiSequence::Ai_Travel: { MWMechanics::AiTravel* travel = new AiTravel( - dynamic_cast(it->mPackage)); + static_cast(it->mPackage)); mPackages.push_back(travel); break; } case ESM::AiSequence::Ai_Escort: { MWMechanics::AiEscort* escort = new AiEscort( - dynamic_cast(it->mPackage)); + static_cast(it->mPackage)); mPackages.push_back(escort); break; } case ESM::AiSequence::Ai_Follow: { MWMechanics::AiFollow* follow = new AiFollow( - dynamic_cast(it->mPackage)); + static_cast(it->mPackage)); mPackages.push_back(follow); break; } case ESM::AiSequence::Ai_Activate: { MWMechanics::AiActivate* activate = new AiActivate( - dynamic_cast(it->mPackage)); + static_cast(it->mPackage)); mPackages.push_back(activate); break; } case ESM::AiSequence::Ai_Combat: { MWMechanics::AiCombat* combat = new AiCombat( - dynamic_cast(it->mPackage)); + static_cast(it->mPackage)); mPackages.push_back(combat); break; } case ESM::AiSequence::Ai_Pursue: { MWMechanics::AiPursue* pursue = new AiPursue( - dynamic_cast(it->mPackage)); + static_cast(it->mPackage)); mPackages.push_back(pursue); break; } @@ -392,4 +393,13 @@ void AiSequence::readState(const ESM::AiSequence::AiSequence &sequence) } } +void AiSequence::fastForward(const MWWorld::Ptr& actor, AiState& state) +{ + if (!mPackages.empty()) + { + MWMechanics::AiPackage* package = mPackages.front(); + package->fastForward(actor, state); + } +} + } // namespace MWMechanics diff --git a/apps/openmw/mwmechanics/aisequence.hpp b/apps/openmw/mwmechanics/aisequence.hpp index bc20dc61b..e43ce72f1 100644 --- a/apps/openmw/mwmechanics/aisequence.hpp +++ b/apps/openmw/mwmechanics/aisequence.hpp @@ -4,6 +4,7 @@ #include #include +//#include "aistate.hpp" namespace MWWorld { @@ -18,9 +19,15 @@ namespace ESM } } + + namespace MWMechanics { class AiPackage; + + template< class Base > class DerivedClassStorage; + struct AiTemporaryBase; + typedef DerivedClassStorage AiState; /// \brief Sequence of AI-packages for a single actor /** The top-most AI package is run each frame. When completed, it is removed from the stack. **/ @@ -50,6 +57,12 @@ namespace MWMechanics virtual ~AiSequence(); + /// Iterator may be invalidated by any function calls other than begin() or end(). + std::list::const_iterator begin() const; + std::list::const_iterator end() const; + + std::list::const_iterator erase (std::list::const_iterator package); + /// Returns currently executing AiPackage type /** \see enum AiPackage::TypeId **/ int getTypeId() const; @@ -82,7 +95,10 @@ namespace MWMechanics void stopPursuit(); /// Execute current package, switching if needed. - void execute (const MWWorld::Ptr& actor,float duration); + void execute (const MWWorld::Ptr& actor, MWMechanics::AiState& state, float duration); + + /// Simulate the passing of time using the currently active AI package + void fastForward(const MWWorld::Ptr &actor, AiState &state); /// Remove all packages. void clear(); @@ -92,10 +108,6 @@ namespace MWMechanics @param actor The actor that owns this AiSequence **/ void stack (const AiPackage& package, const MWWorld::Ptr& actor); - /// Add \a package to the end of the sequence - /** Executed after all other packages have been completed **/ - void queue (const AiPackage& package); - /// Return the current active package. /** If there is no active package, it will throw an exception **/ AiPackage* getActivePackage(); diff --git a/apps/openmw/mwmechanics/aistate.hpp b/apps/openmw/mwmechanics/aistate.hpp new file mode 100644 index 000000000..581f45d07 --- /dev/null +++ b/apps/openmw/mwmechanics/aistate.hpp @@ -0,0 +1,118 @@ +#ifndef AISTATE_H +#define AISTATE_H + +#include +#include + +// c++11 replacement +#include +#include + +namespace MWMechanics +{ + + /** \brief stores one object of any class derived from Base. + * Requesting a certain dereived class via get() either returns + * the stored object if it has the correct type or otherwise replaces + * it with an object of the requested type. + */ + template< class Base > + class DerivedClassStorage + { + private: + Base* mStorage; + + // assert that Derived is derived from Base. + template< class Derived > + void assert_derived() + { + // c++11: + // static_assert( std::is_base_of , "DerivedClassStorage may only store derived classes" ); + + // boost: + BOOST_STATIC_ASSERT((boost::is_base_of::value));//,"DerivedClassStorage may only store derived classes"); + } + + //if needed you have to provide a clone member function + DerivedClassStorage( const DerivedClassStorage& other ); + DerivedClassStorage& operator=( const DerivedClassStorage& ); + + public: + /// \brief returns reference to stored object or deletes it and creates a fitting + template< class Derived > + Derived& get() + { + assert_derived(); + + Derived* result = dynamic_cast(mStorage); + + if(!result) + { + if(mStorage) + delete mStorage; + mStorage = result = new Derived(); + } + + //return a reference to the (new allocated) object + return *result; + } + + template< class Derived > + void store( const Derived& payload ) + { + assert_derived(); + if(mStorage) + delete mStorage; + mStorage = new Derived(payload); + } + + /// \brief takes ownership of the passed object + template< class Derived > + void moveIn( Derived* p ) + { + assert_derived(); + if(mStorage) + delete mStorage; + mStorage = p; + } + + bool empty() const + { + return mStorage == NULL; + } + + const std::type_info& getType() const + { + return typeid(mStorage); + } + + + DerivedClassStorage():mStorage(NULL){}; + ~DerivedClassStorage() + { + if(mStorage) + delete mStorage; + }; + + + + }; + + + /// \brief base class for the temporary storage of AiPackages. + /** + * Each AI package with temporary values needs a AiPackageStorage class + * which is derived from AiTemporaryBase. The Actor holds a container + * AiState where one of these storages can be stored at a time. + * The execute(...) member function takes this container as an argument. + * */ + struct AiTemporaryBase + { + virtual ~AiTemporaryBase(){}; + }; + + /// \brief Container for AI package status. + typedef DerivedClassStorage AiState; +} + +#endif // AISTATE_H diff --git a/apps/openmw/mwmechanics/aitravel.cpp b/apps/openmw/mwmechanics/aitravel.cpp index db137037d..7124a1102 100644 --- a/apps/openmw/mwmechanics/aitravel.cpp +++ b/apps/openmw/mwmechanics/aitravel.cpp @@ -1,5 +1,7 @@ #include "aitravel.hpp" +#include + #include #include "../mwbase/world.hpp" @@ -12,10 +14,23 @@ #include "movement.hpp" #include "creaturestats.hpp" +namespace +{ + +bool isWithinMaxRange(const Ogre::Vector3& pos1, const Ogre::Vector3& pos2) +{ + // Maximum travel distance for vanilla compatibility. + // Was likely meant to prevent NPCs walking into non-loaded exterior cells, but for some reason is used in interior cells as well. + // We can make this configurable at some point, but the default *must* be the below value. Anything else will break shoddily-written content (*cough* MW *cough*) in bizarre ways. + return (pos1.squaredDistance(pos2) <= 7168*7168); +} + +} + namespace MWMechanics { AiTravel::AiTravel(float x, float y, float z) - : mX(x),mY(y),mZ(z),mPathFinder() + : mX(x),mY(y),mZ(z) , mCellX(std::numeric_limits::max()) , mCellY(std::numeric_limits::max()) { @@ -23,7 +38,6 @@ namespace MWMechanics AiTravel::AiTravel(const ESM::AiSequence::AiTravel *travel) : mX(travel->mData.mX), mY(travel->mData.mY), mZ(travel->mData.mZ) - , mPathFinder() , mCellX(std::numeric_limits::max()) , mCellY(std::numeric_limits::max()) { @@ -35,7 +49,7 @@ namespace MWMechanics return new AiTravel(*this); } - bool AiTravel::execute (const MWWorld::Ptr& actor,float duration) + bool AiTravel::execute (const MWWorld::Ptr& actor, AiState& state, float duration) { MWBase::World *world = MWBase::Environment::get().getWorld(); ESM::Position pos = actor.getRefData().getPosition(); @@ -44,6 +58,8 @@ namespace MWMechanics actor.getClass().getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, false); + actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Nothing); + MWWorld::Ptr player = world->getPlayerPtr(); if(cell->mData.mX != player.getCell()->getCell()->mData.mX) { @@ -68,6 +84,9 @@ namespace MWMechanics } } + if (!isWithinMaxRange(Ogre::Vector3(mX, mY, mZ), Ogre::Vector3(pos.pos))) + return false; + bool cellChange = cell->mData.mX != mCellX || cell->mData.mY != mCellY; if(!mPathFinder.isPathConstructed() || cellChange) { @@ -104,6 +123,16 @@ namespace MWMechanics return TypeIdTravel; } + void AiTravel::fastForward(const MWWorld::Ptr& actor, AiState& state) + { + if (!isWithinMaxRange(Ogre::Vector3(mX, mY, mZ), Ogre::Vector3(actor.getRefData().getPosition().pos))) + return; + // does not do any validation on the travel target (whether it's in air, inside collision geometry, etc), + // that is the user's responsibility + MWBase::Environment::get().getWorld()->moveObject(actor, mX, mY, mZ); + actor.getClass().adjustPosition(actor, false); + } + void AiTravel::writeState(ESM::AiSequence::AiSequence &sequence) const { std::auto_ptr travel(new ESM::AiSequence::AiTravel()); diff --git a/apps/openmw/mwmechanics/aitravel.hpp b/apps/openmw/mwmechanics/aitravel.hpp index 91ee30253..c2732e3aa 100644 --- a/apps/openmw/mwmechanics/aitravel.hpp +++ b/apps/openmw/mwmechanics/aitravel.hpp @@ -23,11 +23,14 @@ namespace MWMechanics AiTravel(float x, float y, float z); AiTravel(const ESM::AiSequence::AiTravel* travel); + /// Simulates the passing of time + virtual void fastForward(const MWWorld::Ptr& actor, AiState& state); + void writeState(ESM::AiSequence::AiSequence &sequence) const; virtual AiTravel *clone() const; - virtual bool execute (const MWWorld::Ptr& actor,float duration); + virtual bool execute (const MWWorld::Ptr& actor, AiState& state, float duration); virtual int getTypeId() const; @@ -39,7 +42,6 @@ namespace MWMechanics int mCellX; int mCellY; - PathFinder mPathFinder; }; } diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index b97554e2a..fbb147b34 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -9,6 +9,7 @@ #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/dialoguemanager.hpp" +#include "../mwbase/soundmanager.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" @@ -18,13 +19,59 @@ #include "steering.hpp" #include "movement.hpp" + + namespace MWMechanics { static const int COUNT_BEFORE_RESET = 200; // TODO: maybe no longer needed static const float DOOR_CHECK_INTERVAL = 1.5f; + static const float REACTION_INTERVAL = 0.25f; + static const int GREETING_SHOULD_START = 4; //how many reaction intervals should pass before NPC can greet player + static const int GREETING_SHOULD_END = 10; + /// \brief This class holds the variables AiWander needs which are deleted if the package becomes inactive. + struct AiWanderStorage : AiTemporaryBase + { + // the z rotation angle (degrees) we want to reach + // used every frame when mRotate is true + Ogre::Radian mTargetAngle; + bool mRotate; + float mReaction; // update some actions infrequently + + + AiWander::GreetingState mSaidGreeting; + int mGreetingTimer; + + const MWWorld::CellStore* mCell; // for detecting cell change + + // AiWander states + bool mChooseAction; + bool mIdleNow; + bool mMoveNow; + bool mWalking; + + unsigned short mPlayedIdle; + + PathFinder mPathFinder; + + AiWanderStorage(): + mTargetAngle(0), + mRotate(false), + mReaction(0), + mSaidGreeting(AiWander::Greet_None), + mGreetingTimer(0), + mCell(NULL), + mChooseAction(true), + mIdleNow(false), + mMoveNow(false), + mWalking(false), + mPlayedIdle(0) + {}; + }; + AiWander::AiWander(int distance, int duration, int timeOfDay, const std::vector& idle, bool repeat): mDistance(distance), mDuration(duration), mTimeOfDay(timeOfDay), mIdle(idle), mRepeat(repeat) + , mStoredInitialActorPosition(false) { mIdle.resize(8, 0); init(); @@ -32,18 +79,13 @@ namespace MWMechanics void AiWander::init() { - mCellX = std::numeric_limits::max(); - mCellY = std::numeric_limits::max(); - mXCell = 0; - mYCell = 0; - mCell = NULL; + // NOTE: mDistance and mDuration must be set already + + mStuckCount = 0;// TODO: maybe no longer needed mDoorCheckDuration = 0; mTrimCurrentNode = false; - mReaction = 0; - mRotate = false; - mTargetAngle = 0; - mSaidGreeting = false; + mHasReturnPosition = false; mReturnPosition = Ogre::Vector3(0,0,0); @@ -55,13 +97,9 @@ namespace MWMechanics mTimeOfDay = 0; mStartTime = MWBase::Environment::get().getWorld()->getTimeStamp(); - mPlayedIdle = 0; mStoredAvailableNodes = false; - mChooseAction = true; - mIdleNow = false; - mMoveNow = false; - mWalking = false; + } AiPackage * MWMechanics::AiWander::clone() const @@ -119,56 +157,67 @@ namespace MWMechanics * actors will enter combat (i.e. no longer wandering) and different pathfinding * will kick in. */ - bool AiWander::execute (const MWWorld::Ptr& actor,float duration) + bool AiWander::execute (const MWWorld::Ptr& actor, AiState& state, float duration) { + // get or create temporary storage + AiWanderStorage& storage = state.get(); + + + const MWWorld::CellStore*& currentCell = storage.mCell; MWMechanics::CreatureStats& cStats = actor.getClass().getCreatureStats(actor); if(cStats.isDead() || cStats.getHealth().getCurrent() <= 0) return true; // Don't bother with dead actors - bool cellChange = mCell && (actor.getCell() != mCell); - if(!mCell || cellChange) + bool cellChange = currentCell && (actor.getCell() != currentCell); + if(!currentCell || cellChange) { - mCell = actor.getCell(); + currentCell = actor.getCell(); mStoredAvailableNodes = false; // prob. not needed since mDistance = 0 } - const ESM::Cell *cell = mCell->getCell(); cStats.setDrawState(DrawState_Nothing); cStats.setMovementFlag(CreatureStats::Flag_Run, false); ESM::Position pos = actor.getRefData().getPosition(); - + + + bool& idleNow = storage.mIdleNow; + bool& moveNow = storage.mMoveNow; + bool& walking = storage.mWalking; // Check if an idle actor is too close to a door - if so start walking mDoorCheckDuration += duration; if(mDoorCheckDuration >= DOOR_CHECK_INTERVAL) { mDoorCheckDuration = 0; // restart timer if(mDistance && // actor is not intended to be stationary - mIdleNow && // but is in idle - !mWalking && // FIXME: some actors are idle while walking + idleNow && // but is in idle + !walking && // FIXME: some actors are idle while walking proximityToDoor(actor, MIN_DIST_TO_DOOR_SQUARED*1.6*1.6)) // NOTE: checks interior cells only { - mIdleNow = false; - mMoveNow = true; + idleNow = false; + moveNow = true; mTrimCurrentNode = false; // just in case } } // Are we there yet? - if(mWalking && - mPathFinder.checkPathCompleted(pos.pos[0], pos.pos[1], pos.pos[2])) + bool& chooseAction = storage.mChooseAction; + if(walking && + storage.mPathFinder.checkPathCompleted(pos.pos[0], pos.pos[1], pos.pos[2], 64.f)) { - stopWalking(actor); - mMoveNow = false; - mWalking = false; - mChooseAction = true; + stopWalking(actor, storage); + moveNow = false; + walking = false; + chooseAction = true; mHasReturnPosition = false; } - if(mWalking) // have not yet reached the destination + + + if(walking) // have not yet reached the destination { // turn towards the next point in mPath - zTurn(actor, Ogre::Degree(mPathFinder.getZAngleToNext(pos.pos[0], pos.pos[1]))); + zTurn(actor, Ogre::Degree(storage.mPathFinder.getZAngleToNext(pos.pos[0], pos.pos[1]))); actor.getClass().getMovementSettings(actor).mPosition[1] = 1; // Returns true if evasive action needs to be taken @@ -179,11 +228,11 @@ namespace MWMechanics { // remove allowed points then select another random destination mTrimCurrentNode = true; - trimAllowedNodes(mAllowedNodes, mPathFinder); + trimAllowedNodes(mAllowedNodes, storage.mPathFinder); mObstacleCheck.clear(); - mPathFinder.clearPath(); - mWalking = false; - mMoveNow = true; + storage.mPathFinder.clearPath(); + walking = false; + moveNow = true; } else // probably walking into another NPC { @@ -192,7 +241,7 @@ namespace MWMechanics actor.getClass().getMovementSettings(actor).mPosition[0] = 1; actor.getClass().getMovementSettings(actor).mPosition[1] = 0.1f; // change the angle a bit, too - zTurn(actor, Ogre::Degree(mPathFinder.getZAngleToNext(pos.pos[0] + 1, pos.pos[1]))); + zTurn(actor, Ogre::Degree(storage.mPathFinder.getZAngleToNext(pos.pos[0] + 1, pos.pos[1]))); } mStuckCount++; // TODO: maybe no longer needed } @@ -203,34 +252,97 @@ namespace MWMechanics //std::cout << "Reset \""<< cls.getName(actor) << "\"" << std::endl; mObstacleCheck.clear(); - stopWalking(actor); - mMoveNow = false; - mWalking = false; - mChooseAction = true; + stopWalking(actor, storage); + moveNow = false; + walking = false; + chooseAction = true; } //#endif } - - if (mRotate) + + + Ogre::Radian& targetAngle = storage.mTargetAngle; + bool& rotate = storage.mRotate; + if (rotate) { // Reduce the turning animation glitch by using a *HUGE* value of // epsilon... TODO: a proper fix might be in either the physics or the // animation subsystem - if (zTurn(actor, Ogre::Degree(mTargetAngle), Ogre::Degree(5))) - mRotate = false; + if (zTurn(actor, targetAngle, Ogre::Degree(5))) + rotate = false; + } + + // Check if idle animation finished + short unsigned& playedIdle = storage.mPlayedIdle; + GreetingState& greetingState = storage.mSaidGreeting; + if(idleNow && !checkIdle(actor, playedIdle) && (greetingState == Greet_Done || greetingState == Greet_None)) + { + playedIdle = 0; + idleNow = false; + chooseAction = true; } - mReaction += duration; - if(mReaction < 0.25f) // FIXME: hard coded constant + MWBase::World *world = MWBase::Environment::get().getWorld(); + + if(chooseAction) + { + playedIdle = 0; + getRandomIdle(playedIdle); // NOTE: sets mPlayedIdle with a random selection + + if(!playedIdle && mDistance) + { + chooseAction = false; + moveNow = true; + } + else + { + // Play idle animation and recreate vanilla (broken?) behavior of resetting start time of AIWander: + MWWorld::TimeStamp currentTime = world->getTimeStamp(); + mStartTime = currentTime; + playIdle(actor, playedIdle); + chooseAction = false; + idleNow = true; + } + } + + // Play idle voiced dialogue entries randomly + int hello = cStats.getAiSetting(CreatureStats::AI_Hello).getModified(); + if (hello > 0 && !MWBase::Environment::get().getWorld()->isSwimming(actor) + && MWBase::Environment::get().getSoundManager()->sayDone(actor)) + { + MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); + + static float fVoiceIdleOdds = MWBase::Environment::get().getWorld()->getStore() + .get().find("fVoiceIdleOdds")->getFloat(); + + float roll = std::rand()/ (static_cast (RAND_MAX) + 1) * 10000; + + // In vanilla MW the chance was FPS dependent, and did not allow proper changing of fVoiceIdleOdds + // due to the roll being an integer. + // Our implementation does not have these issues, so needs to be recalibrated. We chose to + // use the chance MW would have when run at 60 FPS with the default value of the GMST for calibration. + float x = fVoiceIdleOdds * 0.6 * (MWBase::Environment::get().getFrameDuration() / 0.1); + + // Only say Idle voices when player is in LOS + // A bit counterintuitive, likely vanilla did this to reduce the appearance of + // voices going through walls? + if (roll < x && Ogre::Vector3(player.getRefData().getPosition().pos).squaredDistance(Ogre::Vector3(pos.pos)) + < 3000*3000 // maybe should be fAudioVoiceDefaultMaxDistance*fAudioMaxDistanceMult instead + && MWBase::Environment::get().getWorld()->getLOS(player, actor)) + MWBase::Environment::get().getDialogueManager()->say(actor, "idle"); + } + + float& lastReaction = storage.mReaction; + lastReaction += duration; + if(lastReaction < REACTION_INTERVAL) { return false; } else - mReaction = 0; + lastReaction = 0; - // NOTE: everything below get updated every 0.25 seconds + // NOTE: everything below get updated every REACTION_INTERVAL seconds - MWBase::World *world = MWBase::Environment::get().getWorld(); if(mDuration) { // End package if duration is complete or mid-night hits: @@ -239,7 +351,7 @@ namespace MWMechanics { if(!mRepeat) { - stopWalking(actor); + stopWalking(actor, storage); return true; } else @@ -249,7 +361,7 @@ namespace MWMechanics { if(!mRepeat) { - stopWalking(actor); + stopWalking(actor, storage); return true; } else @@ -260,72 +372,7 @@ namespace MWMechanics // Initialization to discover & store allowed node points for this actor. if(!mStoredAvailableNodes) { - // infrequently used, therefore no benefit in caching it as a member - const ESM::Pathgrid * - pathgrid = world->getStore().get().search(*cell); - - // cache the current cell location - mCellX = cell->mData.mX; - mCellY = cell->mData.mY; - - // If there is no path this actor doesn't go anywhere. See: - // https://forum.openmw.org/viewtopic.php?t=1556 - // http://www.fliggerty.com/phpBB3/viewtopic.php?f=30&t=5833 - if(!pathgrid || pathgrid->mPoints.empty()) - mDistance = 0; - - // A distance value passed into the constructor indicates how far the - // actor can wander from the spawn position. AiWander assumes that - // pathgrid points are available, and uses them to randomly select wander - // destinations within the allowed set of pathgrid points (nodes). - if(mDistance) - { - mXCell = 0; - mYCell = 0; - if(cell->isExterior()) - { - mXCell = mCellX * ESM::Land::REAL_SIZE; - mYCell = mCellY * ESM::Land::REAL_SIZE; - } - - // FIXME: There might be a bug here. The allowed node points are - // based on the actor's current position rather than the actor's - // spawn point. As a result the allowed nodes for wander can change - // between saves, for example. - // - // convert npcPos to local (i.e. cell) co-ordinates - Ogre::Vector3 npcPos(pos.pos); - npcPos[0] = npcPos[0] - mXCell; - npcPos[1] = npcPos[1] - mYCell; - - // mAllowedNodes for this actor with pathgrid point indexes based on mDistance - // NOTE: mPoints and mAllowedNodes are in local co-ordinates - for(unsigned int counter = 0; counter < pathgrid->mPoints.size(); counter++) - { - Ogre::Vector3 nodePos(pathgrid->mPoints[counter].mX, pathgrid->mPoints[counter].mY, - pathgrid->mPoints[counter].mZ); - if(npcPos.squaredDistance(nodePos) <= mDistance * mDistance) - mAllowedNodes.push_back(pathgrid->mPoints[counter]); - } - if(!mAllowedNodes.empty()) - { - Ogre::Vector3 firstNodePos(mAllowedNodes[0].mX, mAllowedNodes[0].mY, mAllowedNodes[0].mZ); - float closestNode = npcPos.squaredDistance(firstNodePos); - unsigned int index = 0; - for(unsigned int counterThree = 1; counterThree < mAllowedNodes.size(); counterThree++) - { - Ogre::Vector3 nodePos(mAllowedNodes[counterThree].mX, mAllowedNodes[counterThree].mY, - mAllowedNodes[counterThree].mZ); - float tempDist = npcPos.squaredDistance(nodePos); - if(tempDist < closestNode) - index = counterThree; - } - mCurrentNode = mAllowedNodes[index]; - mAllowedNodes.erase(mAllowedNodes.begin() + index); - - mStoredAvailableNodes = true; // set only if successful in finding allowed nodes - } - } + getAllowedNodes(actor, currentCell->getCell()); } // Actor becomes stationary - see above URL's for previous research @@ -341,10 +388,10 @@ namespace MWMechanics mHasReturnPosition = false; if (mDistance == 0 && mHasReturnPosition && Ogre::Vector3(pos.pos).squaredDistance(mReturnPosition) > 20*20) { - mChooseAction = false; - mIdleNow = false; + chooseAction = false; + idleNow = false; - if (!mPathFinder.isPathConstructed()) + if (!storage.mPathFinder.isPathConstructed()) { Ogre::Vector3 destNodePos = mReturnPosition; @@ -360,54 +407,18 @@ namespace MWMechanics start.mZ = pos.pos[2]; // don't take shortcuts for wandering - mPathFinder.buildPath(start, dest, actor.getCell(), false); - - if(mPathFinder.isPathConstructed()) - { - mMoveNow = false; - mWalking = true; - } - } - } - - if(mChooseAction) - { - mPlayedIdle = 0; - getRandomIdle(); // NOTE: sets mPlayedIdle with a random selection - - if(!mPlayedIdle && mDistance) - { - mChooseAction = false; - mMoveNow = true; - } - else - { - // Play idle animation and recreate vanilla (broken?) behavior of resetting start time of AIWander: - MWWorld::TimeStamp currentTime = world->getTimeStamp(); - mStartTime = currentTime; - playIdle(actor, mPlayedIdle); - mChooseAction = false; - mIdleNow = true; + storage.mPathFinder.buildPath(start, dest, actor.getCell(), false); - // Play idle voiced dialogue entries randomly - int hello = cStats.getAiSetting(CreatureStats::AI_Hello).getModified(); - if (hello > 0) + if(storage.mPathFinder.isPathConstructed()) { - int roll = std::rand()/ (static_cast (RAND_MAX) + 1) * 100; // [0, 99] - MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); - - // Don't bother if the player is out of hearing range - static float fVoiceIdleOdds = MWBase::Environment::get().getWorld()->getStore() - .get().find("fVoiceIdleOdds")->getFloat(); - - if (roll < fVoiceIdleOdds && Ogre::Vector3(player.getRefData().getPosition().pos).squaredDistance(Ogre::Vector3(pos.pos)) < 1500*1500) - MWBase::Environment::get().getDialogueManager()->say(actor, "idle"); + moveNow = false; + walking = true; } } } // Allow interrupting a walking actor to trigger a greeting - if(mIdleNow || (mWalking && !mObstacleCheck.isNormalState() && mDistance)) + if(idleNow || walking) { // Play a random voice greeting if the player gets too close int hello = cStats.getAiSetting(CreatureStats::AI_Hello).getModified(); @@ -422,69 +433,69 @@ namespace MWMechanics Ogre::Vector3 actorPos(actor.getRefData().getPosition().pos); float playerDistSqr = playerPos.squaredDistance(actorPos); - if(playerDistSqr <= helloDistance*helloDistance) + int& greetingTimer = storage.mGreetingTimer; + if (greetingState == Greet_None) + { + if ((playerDistSqr <= helloDistance*helloDistance) && + !player.getClass().getCreatureStats(player).isDead() && MWBase::Environment::get().getWorld()->getLOS(player, actor) + && MWBase::Environment::get().getMechanicsManager()->awarenessCheck(player, actor)) + greetingTimer++; + + if (greetingTimer >= GREETING_SHOULD_START) + { + greetingState = Greet_InProgress; + MWBase::Environment::get().getDialogueManager()->say(actor, "hello"); + greetingTimer = 0; + } + } + + if(greetingState == Greet_InProgress) { - if(mWalking) + greetingTimer++; + + if(walking) { - stopWalking(actor); - mMoveNow = false; - mWalking = false; + stopWalking(actor, storage); + moveNow = false; + walking = false; mObstacleCheck.clear(); - mIdleNow = true; - getRandomIdle(); + idleNow = true; + getRandomIdle(playedIdle); } - if(!mRotate) + if(!rotate) { Ogre::Vector3 dir = playerPos - actorPos; - float length = dir.length(); - float faceAngle = Ogre::Radian(Ogre::Math::ACos(dir.y / length) * - ((Ogre::Math::ASin(dir.x / length).valueRadians()>0)?1.0:-1.0)).valueDegrees(); - float actorAngle = actor.getRefData().getBaseNode()->getOrientation().getRoll().valueDegrees(); + Ogre::Radian faceAngle = Ogre::Math::ATan2(dir.x,dir.y); + Ogre::Radian actorAngle = actor.getRefData().getBaseNode()->getOrientation().getRoll(); // an attempt at reducing the turning animation glitch - if(abs(abs(faceAngle) - abs(actorAngle)) >= 5) // TODO: is there a better way? + if( Ogre::Math::Abs( faceAngle - actorAngle ) >= Ogre::Degree(5) ) // TODO: is there a better way? { - mTargetAngle = faceAngle; - mRotate = true; + targetAngle = faceAngle; + rotate = true; } } - } - - if (!mSaidGreeting) - { - // TODO: check if actor is aware / has line of sight - if (playerDistSqr <= helloDistance*helloDistance - // Only play a greeting if the player is not moving - && Ogre::Vector3(player.getClass().getMovementSettings(player).mPosition).squaredLength() == 0) + + if (greetingTimer >= GREETING_SHOULD_END) { - mSaidGreeting = true; - MWBase::Environment::get().getDialogueManager()->say(actor, "hello"); + greetingState = Greet_Done; + greetingTimer = 0; } } - else + + if (greetingState == MWMechanics::AiWander::Greet_Done) { - static float fGreetDistanceReset = MWBase::Environment::get().getWorld()->getStore() - .get().find("fGreetDistanceReset")->getFloat(); - - if (playerDistSqr >= fGreetDistanceReset*fGreetDistanceReset * iGreetDistanceMultiplier*iGreetDistanceMultiplier) - mSaidGreeting = false; - } - - // Check if idle animation finished - // FIXME: don't stay forever - if(!checkIdle(actor, mPlayedIdle) && playerDistSqr > helloDistance*helloDistance) - { - mPlayedIdle = 0; - mIdleNow = false; - mChooseAction = true; + float resetDist = 2*helloDistance; + if (playerDistSqr >= resetDist*resetDist) + greetingState = Greet_None; } } - if(mMoveNow && mDistance) + if(moveNow && mDistance) { // Construct a new path if there isn't one - if(!mPathFinder.isPathConstructed()) + if(!storage.mPathFinder.isPathConstructed()) { assert(mAllowedNodes.size()); unsigned int randNode = (int)(rand() / ((double)RAND_MAX + 1) * mAllowedNodes.size()); @@ -495,9 +506,14 @@ namespace MWMechanics // convert dest to use world co-ordinates ESM::Pathgrid::Point dest; - dest.mX = destNodePos[0] + mXCell; - dest.mY = destNodePos[1] + mYCell; + dest.mX = destNodePos[0]; + dest.mY = destNodePos[1]; dest.mZ = destNodePos[2]; + if (currentCell->getCell()->isExterior()) + { + dest.mX += currentCell->getCell()->mData.mX * ESM::Land::REAL_SIZE; + dest.mY += currentCell->getCell()->mData.mY * ESM::Land::REAL_SIZE; + } // actor position is already in world co-ordinates ESM::Pathgrid::Point start; @@ -506,16 +522,16 @@ namespace MWMechanics start.mZ = pos.pos[2]; // don't take shortcuts for wandering - mPathFinder.buildPath(start, dest, actor.getCell(), false); + storage.mPathFinder.buildPath(start, dest, actor.getCell(), false); - if(mPathFinder.isPathConstructed()) + if(storage.mPathFinder.isPathConstructed()) { // buildPath inserts dest in case it is not a pathgraph point // index which is a duplicate for AiWander. However below code // does not work since getPath() returns a copy of path not a // reference - //if(mPathFinder.getPathSize() > 1) - //mPathFinder.getPath().pop_back(); + //if(storage.mPathFinder.getPathSize() > 1) + //storage.mPathFinder.getPath().pop_back(); // Remove this node as an option and add back the previously used node (stops NPC from picking the same node): ESM::Pathgrid::Point temp = mAllowedNodes[randNode]; @@ -527,13 +543,13 @@ namespace MWMechanics mAllowedNodes.push_back(mCurrentNode); mCurrentNode = temp; - mMoveNow = false; - mWalking = true; + moveNow = false; + walking = true; } // Choose a different node and delete this one from possible nodes because it is uncreachable: else mAllowedNodes.erase(mAllowedNodes.begin() + randNode); - } + } } return false; // AiWander package not yet completed @@ -570,9 +586,9 @@ namespace MWMechanics return TypeIdWander; } - void AiWander::stopWalking(const MWWorld::Ptr& actor) + void AiWander::stopWalking(const MWWorld::Ptr& actor, AiWanderStorage& storage) { - mPathFinder.clearPath(); + storage.mPathFinder.clearPath(); actor.getClass().getMovementSettings(actor).mPosition[1] = 0; } @@ -627,7 +643,7 @@ namespace MWMechanics } } - void AiWander::getRandomIdle() + void AiWander::getRandomIdle(short unsigned& playedIdle) { unsigned short idleRoll = 0; @@ -640,12 +656,109 @@ namespace MWMechanics unsigned short randSelect = (int)(rand() / ((double)RAND_MAX + 1) * int(100 / fIdleChanceMultiplier)); if(randSelect < idleChance && randSelect > idleRoll) { - mPlayedIdle = counter+2; + playedIdle = counter+2; idleRoll = randSelect; } } } + void AiWander::fastForward(const MWWorld::Ptr& actor, AiState &state) + { + if (mDistance == 0) + return; + + if (!mStoredAvailableNodes) + getAllowedNodes(actor, actor.getCell()->getCell()); + + if (mAllowedNodes.empty()) + return; + + state.moveIn(new AiWanderStorage()); + + int index = std::rand() / (static_cast (RAND_MAX) + 1) * mAllowedNodes.size(); + ESM::Pathgrid::Point dest = mAllowedNodes[index]; + + // apply a slight offset to prevent overcrowding + dest.mX += Ogre::Math::RangeRandom(-64, 64); + dest.mY += Ogre::Math::RangeRandom(-64, 64); + + if (actor.getCell()->isExterior()) + { + dest.mX += actor.getCell()->getCell()->mData.mX * ESM::Land::REAL_SIZE; + dest.mY += actor.getCell()->getCell()->mData.mY * ESM::Land::REAL_SIZE; + } + + MWBase::Environment::get().getWorld()->moveObject(actor, dest.mX, dest.mY, dest.mZ); + actor.getClass().adjustPosition(actor, false); + } + + void AiWander::getAllowedNodes(const MWWorld::Ptr& actor, const ESM::Cell* cell) + { + if (!mStoredInitialActorPosition) + { + mInitialActorPosition = Ogre::Vector3(actor.getRefData().getPosition().pos); + mStoredInitialActorPosition = true; + } + + // infrequently used, therefore no benefit in caching it as a member + const ESM::Pathgrid * + pathgrid = MWBase::Environment::get().getWorld()->getStore().get().search(*cell); + + // If there is no path this actor doesn't go anywhere. See: + // https://forum.openmw.org/viewtopic.php?t=1556 + // http://www.fliggerty.com/phpBB3/viewtopic.php?f=30&t=5833 + if(!pathgrid || pathgrid->mPoints.empty()) + mDistance = 0; + + // A distance value passed into the constructor indicates how far the + // actor can wander from the spawn position. AiWander assumes that + // pathgrid points are available, and uses them to randomly select wander + // destinations within the allowed set of pathgrid points (nodes). + if(mDistance) + { + float cellXOffset = 0; + float cellYOffset = 0; + if(cell->isExterior()) + { + cellXOffset = cell->mData.mX * ESM::Land::REAL_SIZE; + cellYOffset = cell->mData.mY * ESM::Land::REAL_SIZE; + } + + // convert npcPos to local (i.e. cell) co-ordinates + Ogre::Vector3 npcPos(mInitialActorPosition); + npcPos[0] = npcPos[0] - cellXOffset; + npcPos[1] = npcPos[1] - cellYOffset; + + // mAllowedNodes for this actor with pathgrid point indexes based on mDistance + // NOTE: mPoints and mAllowedNodes are in local co-ordinates + for(unsigned int counter = 0; counter < pathgrid->mPoints.size(); counter++) + { + Ogre::Vector3 nodePos(pathgrid->mPoints[counter].mX, pathgrid->mPoints[counter].mY, + pathgrid->mPoints[counter].mZ); + if(npcPos.squaredDistance(nodePos) <= mDistance * mDistance) + mAllowedNodes.push_back(pathgrid->mPoints[counter]); + } + if(!mAllowedNodes.empty()) + { + Ogre::Vector3 firstNodePos(mAllowedNodes[0].mX, mAllowedNodes[0].mY, mAllowedNodes[0].mZ); + float closestNode = npcPos.squaredDistance(firstNodePos); + unsigned int index = 0; + for(unsigned int counterThree = 1; counterThree < mAllowedNodes.size(); counterThree++) + { + Ogre::Vector3 nodePos(mAllowedNodes[counterThree].mX, mAllowedNodes[counterThree].mY, + mAllowedNodes[counterThree].mZ); + float tempDist = npcPos.squaredDistance(nodePos); + if(tempDist < closestNode) + index = counterThree; + } + mCurrentNode = mAllowedNodes[index]; + mAllowedNodes.erase(mAllowedNodes.begin() + index); + + mStoredAvailableNodes = true; // set only if successful in finding allowed nodes + } + } + } + void AiWander::writeState(ESM::AiSequence::AiSequence &sequence) const { std::auto_ptr wander(new ESM::AiSequence::AiWander()); @@ -657,6 +770,9 @@ namespace MWMechanics for (int i=0; i<8; ++i) wander->mData.mIdle[i] = mIdle[i]; wander->mData.mShouldRepeat = mRepeat; + wander->mStoredInitialActorPosition = mStoredInitialActorPosition; + if (mStoredInitialActorPosition) + wander->mInitialActorPosition = mInitialActorPosition; ESM::AiSequence::AiPackageContainer package; package.mType = ESM::AiSequence::Ai_Wander; @@ -665,17 +781,19 @@ namespace MWMechanics } AiWander::AiWander (const ESM::AiSequence::AiWander* wander) + : mDistance(wander->mData.mDistance) + , mDuration(wander->mData.mDuration) + , mStartTime(MWWorld::TimeStamp(wander->mStartTime)) + , mTimeOfDay(wander->mData.mTimeOfDay) + , mRepeat(wander->mData.mShouldRepeat) + , mStoredInitialActorPosition(wander->mStoredInitialActorPosition) { - init(); - - mDistance = wander->mData.mDistance; - mDuration = wander->mData.mDuration; - mStartTime = MWWorld::TimeStamp(wander->mStartTime); - mTimeOfDay = wander->mData.mTimeOfDay; + if (mStoredInitialActorPosition) + mInitialActorPosition = wander->mInitialActorPosition; for (int i=0; i<8; ++i) mIdle.push_back(wander->mData.mIdle[i]); - mRepeat = wander->mData.mShouldRepeat; + init(); } } diff --git a/apps/openmw/mwmechanics/aiwander.hpp b/apps/openmw/mwmechanics/aiwander.hpp index 7abd19e27..5e1b41813 100644 --- a/apps/openmw/mwmechanics/aiwander.hpp +++ b/apps/openmw/mwmechanics/aiwander.hpp @@ -12,8 +12,12 @@ #include "../mwworld/timestamp.hpp" + +#include "aistate.hpp" + namespace ESM { + class Cell; namespace AiSequence { struct AiWander; @@ -21,7 +25,11 @@ namespace ESM } namespace MWMechanics -{ +{ + + + struct AiWanderStorage; + /// \brief Causes the Actor to wander within a specified range class AiWander : public AiPackage { @@ -36,11 +44,11 @@ namespace MWMechanics AiWander (const ESM::AiSequence::AiWander* wander); - void init(); + virtual AiPackage *clone() const; - virtual bool execute (const MWWorld::Ptr& actor,float duration); + virtual bool execute (const MWWorld::Ptr& actor, AiState& state, float duration); virtual int getTypeId() const; @@ -50,64 +58,65 @@ namespace MWMechanics virtual void writeState(ESM::AiSequence::AiSequence &sequence) const; + virtual void fastForward(const MWWorld::Ptr& actor, AiState& state); + + enum GreetingState { + Greet_None, + Greet_InProgress, + Greet_Done + }; private: - void stopWalking(const MWWorld::Ptr& actor); + // NOTE: mDistance and mDuration must be set already + void init(); + + void stopWalking(const MWWorld::Ptr& actor, AiWanderStorage& storage); void playIdle(const MWWorld::Ptr& actor, unsigned short idleSelect); bool checkIdle(const MWWorld::Ptr& actor, unsigned short idleSelect); - void getRandomIdle(); + void getRandomIdle(unsigned short& playedIdle); int mDistance; // how far the actor can wander from the spawn point int mDuration; int mTimeOfDay; std::vector mIdle; bool mRepeat; - - bool mSaidGreeting; + bool mHasReturnPosition; // NOTE: Could be removed if mReturnPosition was initialized to actor position, // if we had the actor in the AiWander constructor... Ogre::Vector3 mReturnPosition; - // Cached current cell location - int mCellX; - int mCellY; - // Cell location multiplied by ESM::Land::REAL_SIZE - float mXCell; - float mYCell; + Ogre::Vector3 mInitialActorPosition; + bool mStoredInitialActorPosition; - const MWWorld::CellStore* mCell; // for detecting cell change + // if false triggers calculating allowed nodes based on mDistance bool mStoredAvailableNodes; - // AiWander states - bool mChooseAction; - bool mIdleNow; - bool mMoveNow; - bool mWalking; - unsigned short mPlayedIdle; + + MWWorld::TimeStamp mStartTime; // allowed pathgrid nodes based on mDistance from the spawn point std::vector mAllowedNodes; + + void getAllowedNodes(const MWWorld::Ptr& actor, const ESM::Cell* cell); + ESM::Pathgrid::Point mCurrentNode; bool mTrimCurrentNode; void trimAllowedNodes(std::vector& nodes, const PathFinder& pathfinder); - PathFinder mPathFinder; - ObstacleCheck mObstacleCheck; +// ObstacleCheck mObstacleCheck; float mDoorCheckDuration; int mStuckCount; - // the z rotation angle (degrees) we want to reach - // used every frame when mRotate is true - float mTargetAngle; - bool mRotate; - float mReaction; // update some actions infrequently + }; + + } #endif diff --git a/apps/openmw/mwmechanics/alchemy.cpp b/apps/openmw/mwmechanics/alchemy.cpp index 2e03122d5..f3d376a70 100644 --- a/apps/openmw/mwmechanics/alchemy.cpp +++ b/apps/openmw/mwmechanics/alchemy.cpp @@ -27,6 +27,11 @@ #include "creaturestats.hpp" #include "npcstats.hpp" +MWMechanics::Alchemy::Alchemy() + : mValue(0) +{ +} + std::set MWMechanics::Alchemy::listEffects() const { std::map effects; @@ -128,7 +133,7 @@ void MWMechanics::Alchemy::updateEffects() std::set effects (listEffects()); // general alchemy factor - float x = getChance(); + float x = getAlchemyFactor(); x *= mTools[ESM::Apparatus::MortarPestle].get()->mBase->mData.mQuality; x *= MWBase::Environment::get().getWorld()->getStore().get().find ("fPotionStrengthMult")->getFloat(); @@ -181,7 +186,13 @@ void MWMechanics::Alchemy::updateEffects() ESM::ENAMstruct effect; effect.mEffectID = iter->mId; - effect.mSkill = effect.mAttribute = iter->mArg; // somewhat hack-ish, but should work + effect.mAttribute = -1; + effect.mSkill = -1; + + if (magicEffect->mData.mFlags & ESM::MagicEffect::TargetSkill) + effect.mSkill = iter->mArg; + else if (magicEffect->mData.mFlags & ESM::MagicEffect::TargetAttribute) + effect.mAttribute = iter->mArg; effect.mRange = 0; effect.mArea = 0; @@ -194,7 +205,7 @@ void MWMechanics::Alchemy::updateEffects() } } -const ESM::Potion *MWMechanics::Alchemy::getRecord() const +const ESM::Potion *MWMechanics::Alchemy::getRecord(const ESM::Potion& toFind) const { const MWWorld::Store &potions = MWBase::Environment::get().getWorld()->getStore().get(); @@ -205,6 +216,18 @@ const ESM::Potion *MWMechanics::Alchemy::getRecord() const if (iter->mEffects.mList.size() != mEffects.size()) continue; + if (iter->mName != toFind.mName + || iter->mScript != toFind.mScript + || iter->mData.mWeight != toFind.mData.mWeight + || iter->mData.mValue != toFind.mData.mValue + || iter->mData.mAutoCalc != toFind.mData.mAutoCalc) + continue; + + // Don't choose an ID that came from the content files, would have unintended side effects + // where alchemy can be used to produce quest-relevant items + if (!potions.isDynamic(iter->mId)) + continue; + bool mismatch = false; for (int i=0; i (iter->mEffects.mList.size()); ++i) @@ -255,37 +278,35 @@ void MWMechanics::Alchemy::removeIngredients() void MWMechanics::Alchemy::addPotion (const std::string& name) { - const ESM::Potion *record = getRecord(); - - if (!record) - { - ESM::Potion newRecord; + ESM::Potion newRecord; - newRecord.mData.mWeight = 0; + newRecord.mData.mWeight = 0; - for (TIngredientsIterator iter (beginIngredients()); iter!=endIngredients(); ++iter) - if (!iter->isEmpty()) - newRecord.mData.mWeight += iter->get()->mBase->mData.mWeight; + for (TIngredientsIterator iter (beginIngredients()); iter!=endIngredients(); ++iter) + if (!iter->isEmpty()) + newRecord.mData.mWeight += iter->get()->mBase->mData.mWeight; + if (countIngredients() > 0) newRecord.mData.mWeight /= countIngredients(); - newRecord.mData.mValue = mValue; - newRecord.mData.mAutoCalc = 0; + newRecord.mData.mValue = mValue; + newRecord.mData.mAutoCalc = 0; - newRecord.mName = name; + newRecord.mName = name; - int index = static_cast (std::rand()/(static_cast (RAND_MAX)+1)*6); - assert (index>=0 && index<6); + int index = static_cast (std::rand()/(static_cast (RAND_MAX)+1)*6); + assert (index>=0 && index<6); - static const char *name[] = { "standard", "bargain", "cheap", "fresh", "exclusive", "quality" }; + static const char *meshes[] = { "standard", "bargain", "cheap", "fresh", "exclusive", "quality" }; - newRecord.mModel = "m\\misc_potion_" + std::string (name[index]) + "_01.nif"; - newRecord.mIcon = "m\\tx_potion_" + std::string (name[index]) + "_01.dds"; + newRecord.mModel = "m\\misc_potion_" + std::string (meshes[index]) + "_01.nif"; + newRecord.mIcon = "m\\tx_potion_" + std::string (meshes[index]) + "_01.dds"; - newRecord.mEffects.mList = mEffects; + newRecord.mEffects.mList = mEffects; + const ESM::Potion* record = getRecord(newRecord); + if (!record) record = MWBase::Environment::get().getWorld()->createRecord (newRecord); - } mAlchemist.getClass().getContainerStore (mAlchemist).add (record->mId, 1, mAlchemist); } @@ -295,15 +316,15 @@ void MWMechanics::Alchemy::increaseSkill() mAlchemist.getClass().skillUsageSucceeded (mAlchemist, ESM::Skill::Alchemy, 0); } -float MWMechanics::Alchemy::getChance() const +float MWMechanics::Alchemy::getAlchemyFactor() const { const CreatureStats& creatureStats = mAlchemist.getClass().getCreatureStats (mAlchemist); const NpcStats& npcStats = mAlchemist.getClass().getNpcStats (mAlchemist); return (npcStats.getSkill (ESM::Skill::Alchemy).getModified() + - 0.1 * creatureStats.getAttribute (1).getModified() - + 0.1 * creatureStats.getAttribute (7).getModified()); + 0.1 * creatureStats.getAttribute (ESM::Attribute::Intelligence).getModified() + + 0.1 * creatureStats.getAttribute (ESM::Attribute::Luck).getModified()); } int MWMechanics::Alchemy::countIngredients() const @@ -425,14 +446,6 @@ MWMechanics::Alchemy::TEffectsIterator MWMechanics::Alchemy::endEffects() const return mEffects.end(); } -std::string MWMechanics::Alchemy::getPotionName() const -{ - if (const ESM::Potion *potion = getRecord()) - return potion->mName; - - return ""; -} - MWMechanics::Alchemy::Result MWMechanics::Alchemy::create (const std::string& name) { if (mTools[ESM::Apparatus::MortarPestle].isEmpty()) @@ -441,13 +454,20 @@ MWMechanics::Alchemy::Result MWMechanics::Alchemy::create (const std::string& na if (countIngredients()<2) return Result_LessThanTwoIngredients; - if (name.empty() && getPotionName().empty()) + if (name.empty()) return Result_NoName; - if (beginEffects()==endEffects()) + if (listEffects().empty()) return Result_NoEffects; - if (getChance() (RAND_MAX)*100) + if (beginEffects() == endEffects()) + { + // all effects were nullified due to insufficient skill + removeIngredients(); + return Result_RandomFailure; + } + + if (getAlchemyFactor() (RAND_MAX)*100) { removeIngredients(); return Result_RandomFailure; @@ -461,3 +481,14 @@ MWMechanics::Alchemy::Result MWMechanics::Alchemy::create (const std::string& na return Result_Success; } + +std::string MWMechanics::Alchemy::suggestPotionName() +{ + std::set effects = listEffects(); + if (effects.empty()) + return ""; + + int effectId = effects.begin()->mId; + return MWBase::Environment::get().getWorld()->getStore().get().find( + ESM::MagicEffect::effectIdToString(effectId))->getString(); +} diff --git a/apps/openmw/mwmechanics/alchemy.hpp b/apps/openmw/mwmechanics/alchemy.hpp index 31cafa4dc..caba26f14 100644 --- a/apps/openmw/mwmechanics/alchemy.hpp +++ b/apps/openmw/mwmechanics/alchemy.hpp @@ -22,6 +22,8 @@ namespace MWMechanics { public: + Alchemy(); + typedef std::vector TToolsContainer; typedef TToolsContainer::const_iterator TToolsIterator; @@ -50,15 +52,13 @@ namespace MWMechanics TEffectsContainer mEffects; int mValue; - std::set listEffects() const; - ///< List all effects shared by at least two ingredients. - void applyTools (int flags, float& value) const; void updateEffects(); - const ESM::Potion *getRecord() const; - ///< Return existing recrod for created potion (may return 0) + const ESM::Potion *getRecord(const ESM::Potion& toFind) const; + ///< Try to find a potion record similar to \a toFind in the record store, or return 0 if not found + /// \note Does not account for record ID, model or icon void removeIngredients(); ///< Remove selected ingredients from alchemist's inventory, cleanup selected ingredients and @@ -70,11 +70,14 @@ namespace MWMechanics void increaseSkill(); ///< Increase alchemist's skill. - float getChance() const; - ///< Return chance of success. + float getAlchemyFactor() const; int countIngredients() const; + TEffectsIterator beginEffects() const; + + TEffectsIterator endEffects() const; + public: void setAlchemist (const MWWorld::Ptr& npc); @@ -94,6 +97,9 @@ namespace MWMechanics void clear(); ///< Remove alchemist, tools and ingredients. + std::set listEffects() const; + ///< List all effects shared by at least two ingredients. + int addIngredient (const MWWorld::Ptr& ingredient); ///< Add ingredient into the next free slot. /// @@ -103,19 +109,13 @@ namespace MWMechanics void removeIngredient (int index); ///< Remove ingredient from slot (calling this function on an empty slot is a no-op). - TEffectsIterator beginEffects() const; - - TEffectsIterator endEffects() const; - - std::string getPotionName() const; - ///< Return the name of the potion that would be created when calling create (if a record for such - /// a potion already exists) or return an empty string. + std::string suggestPotionName (); + ///< Suggest a name for the potion, based on the current effects Result create (const std::string& name); ///< Try to create a potion from the ingredients, place it in the inventory of the alchemist and /// adjust the skills of the alchemist accordingly. - /// \param name must not be an empty string, unless there is already a potion record ( - /// getPotionName() does not return an empty string). + /// \param name must not be an empty string, or Result_NoName is returned }; } diff --git a/apps/openmw/mwmechanics/autocalcspell.cpp b/apps/openmw/mwmechanics/autocalcspell.cpp index 255decdf7..7b8c43a06 100644 --- a/apps/openmw/mwmechanics/autocalcspell.cpp +++ b/apps/openmw/mwmechanics/autocalcspell.cpp @@ -72,7 +72,7 @@ namespace MWMechanics if (baseMagicka < iAutoSpellTimesCanCast * spell->mData.mCost) continue; - if (race && std::find(race->mPowers.mList.begin(), race->mPowers.mList.end(), spell->mId) != race->mPowers.mList.end()) + if (race && race->mPowers.exists(spell->mId)) continue; if (!attrSkillCheck(spell, actorSkills, actorAttributes)) @@ -220,7 +220,7 @@ namespace MWMechanics if (spell->mData.mFlags & ESM::Spell::F_Always) return 100.f; - float skillTerm; + float skillTerm = 0; if (effectiveSchool != -1) skillTerm = 2.f * actorSkills[mapSchoolToSkill(effectiveSchool)]; else diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 638feeef3..449c030f4 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -27,6 +27,8 @@ #include "creaturestats.hpp" #include "security.hpp" +#include + #include "../mwrender/animation.hpp" #include "../mwbase/environment.hpp" @@ -42,6 +44,15 @@ namespace { +// Wraps a value to (-PI, PI] +void wrap(Ogre::Radian& rad) +{ + if (rad.valueRadians()>0) + rad = Ogre::Radian(std::fmod(rad.valueRadians()+Ogre::Math::PI, 2.0f*Ogre::Math::PI)-Ogre::Math::PI); + else + rad = Ogre::Radian(std::fmod(rad.valueRadians()-Ogre::Math::PI, 2.0f*Ogre::Math::PI)+Ogre::Math::PI); +} + std::string getBestAttack (const ESM::Weapon* weapon) { int slash = (weapon->mData.mSlash[0] + weapon->mData.mSlash[1])/2; @@ -92,6 +103,35 @@ MWMechanics::CharacterState runStateToWalkState (MWMechanics::CharacterState sta return ret; } +float getFallDamage(const MWWorld::Ptr& ptr, float fallHeight) +{ + MWBase::World *world = MWBase::Environment::get().getWorld(); + const MWWorld::Store &store = world->getStore().get(); + + const float fallDistanceMin = store.find("fFallDamageDistanceMin")->getFloat(); + + if (fallHeight >= fallDistanceMin) + { + const float acrobaticsSkill = ptr.getClass().getSkill(ptr, ESM::Skill::Acrobatics); + const float jumpSpellBonus = ptr.getClass().getCreatureStats(ptr).getMagicEffects().get(ESM::MagicEffect::Jump).getMagnitude(); + const float fallAcroBase = store.find("fFallAcroBase")->getFloat(); + const float fallAcroMult = store.find("fFallAcroMult")->getFloat(); + const float fallDistanceBase = store.find("fFallDistanceBase")->getFloat(); + const float fallDistanceMult = store.find("fFallDistanceMult")->getFloat(); + + float x = fallHeight - fallDistanceMin; + x -= (1.5 * acrobaticsSkill) + jumpSpellBonus; + x = std::max(0.0f, x); + + float a = fallAcroBase + fallAcroMult * (100 - acrobaticsSkill); + x = fallDistanceBase + fallDistanceMult * x; + x *= a; + + return x; + } + return 0.f; +} + } namespace MWMechanics @@ -196,7 +236,8 @@ void CharacterController::refreshCurrentAnims(CharacterState idle, CharacterStat bool block = mPtr.getClass().getCreatureStats(mPtr).getBlock(); if(mHitState == CharState_None) { - if (mPtr.getClass().getCreatureStats(mPtr).getFatigue().getCurrent() < 0) + if (mPtr.getClass().getCreatureStats(mPtr).getFatigue().getCurrent() < 0 + || mPtr.getClass().getCreatureStats(mPtr).getFatigue().getBase() == 0) { mHitState = CharState_KnockOut; mCurrentHit = "knockout"; @@ -221,6 +262,21 @@ void CharacterController::refreshCurrentAnims(CharacterState idle, CharacterStat mCurrentHit = "shield"; mAnimation->play(mCurrentHit, Priority_Hit, MWRender::Animation::Group_All, true, 1, "block start", "block stop", 0.0f, 0); } + + // Cancel upper body animations + if (mHitState == CharState_KnockDown || mHitState == CharState_KnockOut) + { + if (mUpperBodyState > UpperCharState_WeapEquiped) + { + mAnimation->disable(mCurrentWeapon); + mUpperBodyState = UpperCharState_WeapEquiped; + } + else if (mUpperBodyState > UpperCharState_Nothing && mUpperBodyState < UpperCharState_WeapEquiped) + { + mAnimation->disable(mCurrentWeapon); + mUpperBodyState = UpperCharState_Nothing; + } + } } else if(!mAnimation->isPlaying(mCurrentHit)) { @@ -242,37 +298,8 @@ void CharacterController::refreshCurrentAnims(CharacterState idle, CharacterStat } const WeaponInfo *weap = std::find_if(sWeaponTypeList, sWeaponTypeListEnd, FindWeaponType(mWeaponType)); - - if(force || idle != mIdleState) - { - mIdleState = idle; - - std::string idle; - // Only play "idleswim" or "idlesneak" if they exist. Otherwise, fallback to - // "idle"+weapon or "idle". - if(mIdleState == CharState_IdleSwim && mAnimation->hasAnimation("idleswim")) - idle = "idleswim"; - else if(mIdleState == CharState_IdleSneak && mAnimation->hasAnimation("idlesneak")) - idle = "idlesneak"; - else if(mIdleState != CharState_None) - { - idle = "idle"; - if(weap != sWeaponTypeListEnd) - { - idle += weap->shortgroup; - if(!mAnimation->hasAnimation(idle)) - idle = "idle"; - } - } - - mAnimation->disable(mCurrentIdle); - mCurrentIdle = idle; - if(!mCurrentIdle.empty()) - mAnimation->play(mCurrentIdle, Priority_Default, MWRender::Animation::Group_All, false, - 1.0f, "start", "stop", 0.0f, ~0ul); - } - - updateIdleStormState(); + if (!mPtr.getClass().isBipedal(mPtr)) + weap = sWeaponTypeListEnd; if(force && mJumpState != JumpState_None) { @@ -292,7 +319,7 @@ void CharacterController::refreshCurrentAnims(CharacterState idle, CharacterStat } } - if(mJumpState == JumpState_Falling) + if(mJumpState == JumpState_InAir) { int mode = ((jump == mCurrentJump) ? 2 : 1); @@ -369,7 +396,8 @@ void CharacterController::refreshCurrentAnims(CharacterState idle, CharacterStat { float vel, speedmult = 1.0f; - bool isrunning = mPtr.getClass().getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Run); + bool isrunning = mPtr.getClass().getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Run) + && !MWBase::Environment::get().getWorld()->isFlying(mPtr); // For non-flying creatures, MW uses the Walk animation to calculate the animation velocity // even if we are running. This must be replicated, otherwise the observed speed would differ drastically. @@ -380,26 +408,79 @@ void CharacterController::refreshCurrentAnims(CharacterState idle, CharacterStat CharacterState walkState = runStateToWalkState(mMovementState); const StateInfo *stateinfo = std::find_if(sMovementList, sMovementListEnd, FindCharState(walkState)); anim = stateinfo->groupname; - } - if(mMovementSpeed > 0.0f && (vel=mAnimation->getVelocity(anim)) > 1.0f) - { - speedmult = mMovementSpeed / vel; + if (mMovementSpeed > 0.0f && (vel=mAnimation->getVelocity(anim)) > 1.0f) + speedmult = mMovementSpeed / vel; + else + // Another bug: when using a fallback animation (e.g. RunForward as fallback to SwimRunForward), + // then the equivalent Walk animation will not use a fallback, and if that animation doesn't exist + // we will play without any scaling. + // Makes the speed attribute of most water creatures totally useless. + // And again, this can not be fixed without patching game data. + speedmult = 1.f; } - else if (mMovementState == CharState_TurnLeft || mMovementState == CharState_TurnRight) - speedmult = 1.f; // TODO: should get a speed mult depending on the current turning speed - else if (mMovementSpeed > 0.0f) + else { - // The first person anims don't have any velocity to calculate a speed multiplier from. - // We use the third person velocities instead. - // FIXME: should be pulled from the actual animation, but it is not presently loaded. - speedmult = mMovementSpeed / (isrunning ? 222.857f : 154.064f); - mMovementAnimationControlled = false; + if(mMovementSpeed > 0.0f && (vel=mAnimation->getVelocity(anim)) > 1.0f) + { + speedmult = mMovementSpeed / vel; + } + else if (mMovementState == CharState_TurnLeft || mMovementState == CharState_TurnRight) + speedmult = 1.f; // adjusted each frame + else if (mMovementSpeed > 0.0f) + { + // The first person anims don't have any velocity to calculate a speed multiplier from. + // We use the third person velocities instead. + // FIXME: should be pulled from the actual animation, but it is not presently loaded. + speedmult = mMovementSpeed / (isrunning ? 222.857f : 154.064f); + mMovementAnimationControlled = false; + } } + mAnimation->play(mCurrentMovement, Priority_Movement, movegroup, false, speedmult, ((mode!=2)?"start":"loop start"), "stop", 0.0f, ~0ul); } } + + // idle handled last as it can depend on the other states + // FIXME: if one of the below states is close to their last animation frame (i.e. will be disabled in the coming update), + // the idle animation should be displayed + if ((mUpperBodyState != UpperCharState_Nothing + || mMovementState != CharState_None + || mHitState != CharState_None) + && !mPtr.getClass().isBipedal(mPtr)) + idle = CharState_None; + + if(force || idle != mIdleState) + { + mIdleState = idle; + + std::string idle; + // Only play "idleswim" or "idlesneak" if they exist. Otherwise, fallback to + // "idle"+weapon or "idle". + if(mIdleState == CharState_IdleSwim && mAnimation->hasAnimation("idleswim")) + idle = "idleswim"; + else if(mIdleState == CharState_IdleSneak && mAnimation->hasAnimation("idlesneak")) + idle = "idlesneak"; + else if(mIdleState != CharState_None) + { + idle = "idle"; + if(weap != sWeaponTypeListEnd) + { + idle += weap->shortgroup; + if(!mAnimation->hasAnimation(idle)) + idle = "idle"; + } + } + + mAnimation->disable(mCurrentIdle); + mCurrentIdle = idle; + if(!mCurrentIdle.empty()) + mAnimation->play(mCurrentIdle, Priority_Default, MWRender::Animation::Group_All, false, + 1.0f, "start", "stop", 0.0f, ~0ul, true); + } + + updateIdleStormState(); } @@ -528,11 +609,11 @@ void CharacterController::playRandomDeath(float startpoint) { mDeathState = CharState_SwimDeath; } - else if (mHitState == CharState_KnockDown) + else if (mHitState == CharState_KnockDown && mAnimation->hasAnimation("deathknockdown")) { mDeathState = CharState_DeathKnockDown; } - else if (mHitState == CharState_KnockOut) + else if (mHitState == CharState_KnockOut && mAnimation->hasAnimation("deathknockout")) { mDeathState = CharState_DeathKnockOut; } @@ -551,6 +632,7 @@ CharacterController::CharacterController(const MWWorld::Ptr &ptr, MWRender::Anim , mIdleState(CharState_None) , mMovementState(CharState_None) , mMovementSpeed(0.0f) + , mHasMovedInXY(false) , mMovementAnimationControlled(true) , mDeathState(CharState_None) , mHitState(CharState_None) @@ -560,6 +642,7 @@ CharacterController::CharacterController(const MWWorld::Ptr &ptr, MWRender::Anim , mSkipAnim(false) , mSecondsOfRunning(0) , mSecondsOfSwimming(0) + , mTurnAnimationThreshold(0) { if(!mAnimation) return; @@ -585,7 +668,8 @@ CharacterController::CharacterController(const MWWorld::Ptr &ptr, MWRender::Anim mAnimation->showWeapons(true); mAnimation->setWeaponGroup(mCurrentWeapon); } - mAnimation->showCarriedLeft(mWeaponType != WeapType_Spell && mWeaponType != WeapType_HandToHand); + + mAnimation->showCarriedLeft(updateCarriedLeftVisible(mWeaponType)); } if(!cls.getCreatureStats(mPtr).isDead()) @@ -636,10 +720,10 @@ void CharacterController::updateIdleStormState() mAnimation->getInfo("idlestorm", &complete); if (complete == 0) - mAnimation->play("idlestorm", Priority_Torch, MWRender::Animation::Group_RightArm, false, + mAnimation->play("idlestorm", Priority_Storm, MWRender::Animation::Group_RightArm, false, 1.0f, "start", "loop start", 0.0f, 0); else if (complete == 1) - mAnimation->play("idlestorm", Priority_Torch, MWRender::Animation::Group_RightArm, false, + mAnimation->play("idlestorm", Priority_Storm, MWRender::Animation::Group_RightArm, false, 1.0f, "loop start", "loop stop", 0.0f, ~0ul); } else @@ -650,7 +734,7 @@ void CharacterController::updateIdleStormState() { if (mAnimation->getCurrentTime("idlestorm") < mAnimation->getTextKeyTime("idlestorm: loop stop")) { - mAnimation->play("idlestorm", Priority_Torch, MWRender::Animation::Group_RightArm, true, + mAnimation->play("idlestorm", Priority_Storm, MWRender::Animation::Group_RightArm, true, 1.0f, "loop stop", "stop", 0.0f, 0); } } @@ -660,32 +744,111 @@ void CharacterController::updateIdleStormState() } } +void CharacterController::castSpell(const std::string &spellid) +{ + static const std::string schools[] = { + "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration" + }; + + const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); + const ESM::Spell *spell = store.get().find(spellid); + const ESM::ENAMstruct &effectentry = spell->mEffects.mList.at(0); + + const ESM::MagicEffect *effect; + effect = store.get().find(effectentry.mEffectID); + + const ESM::Static* castStatic; + if (!effect->mCasting.empty()) + castStatic = store.get().find (effect->mCasting); + else + castStatic = store.get().find ("VFX_DefaultCast"); + + mAnimation->addEffect("meshes\\" + castStatic->mModel, effect->mIndex); + + MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); + if(!effect->mCastSound.empty()) + sndMgr->playSound3D(mPtr, effect->mCastSound, 1.0f, 1.0f); + else + sndMgr->playSound3D(mPtr, schools[effect->mData.mSchool]+" cast", 1.0f, 1.0f); +} + bool CharacterController::updateCreatureState() { const MWWorld::Class &cls = mPtr.getClass(); CreatureStats &stats = cls.getCreatureStats(mPtr); + WeaponType weapType = WeapType_None; + if(stats.getDrawState() == DrawState_Weapon) + weapType = WeapType_HandToHand; + else if (stats.getDrawState() == DrawState_Spell) + weapType = WeapType_Spell; + + if (weapType != mWeaponType) + { + mWeaponType = weapType; + if (mAnimation->isPlaying(mCurrentWeapon)) + mAnimation->disable(mCurrentWeapon); + } + if(stats.getAttackingOrSpell()) { if(mUpperBodyState == UpperCharState_Nothing && mHitState == CharState_None) { MWBase::Environment::get().getWorld()->breakInvisibility(mPtr); - // These are unique animations and not linked to movement type. Just pick one randomly. - int roll = std::rand()/ (static_cast (RAND_MAX) + 1) * 3; // [0, 2] - if (roll == 0) - mCurrentWeapon = "attack1"; - else if (roll == 1) - mCurrentWeapon = "attack2"; - else - mCurrentWeapon = "attack3"; + std::string startKey = "start"; + std::string stopKey = "stop"; + if (weapType == WeapType_Spell) + { + const std::string spellid = stats.getSpells().getSelectedSpell(); + if (!spellid.empty() && MWBase::Environment::get().getWorld()->startSpellCast(mPtr)) + { + castSpell(spellid); - mAnimation->play(mCurrentWeapon, Priority_Weapon, - MWRender::Animation::Group_All, true, - 1, "start", "stop", - 0.0f, 0); - mUpperBodyState = UpperCharState_StartToMinAttack; + if (!mAnimation->hasAnimation("spellcast")) + MWBase::Environment::get().getWorld()->castSpell(mPtr); // No "release" text key to use, so cast immediately + else + { + const ESM::Spell *spell = MWBase::Environment::get().getWorld()->getStore().get().find(spellid); + const ESM::ENAMstruct &effectentry = spell->mEffects.mList.at(0); + + switch(effectentry.mRange) + { + case 0: mAttackType = "self"; break; + case 1: mAttackType = "touch"; break; + case 2: mAttackType = "target"; break; + } + + startKey = mAttackType + " " + startKey; + stopKey = mAttackType + " " + stopKey; + mCurrentWeapon = "spellcast"; + } + } + else + mCurrentWeapon = ""; + } + if (weapType != WeapType_Spell || !mAnimation->hasAnimation("spellcast")) // Not all creatures have a dedicated spellcast animation + { + int roll = std::rand()/ (static_cast (RAND_MAX) + 1) * 3; // [0, 2] + if (roll == 0) + mCurrentWeapon = "attack1"; + else if (roll == 1) + mCurrentWeapon = "attack2"; + else + mCurrentWeapon = "attack3"; + } + + if (!mCurrentWeapon.empty()) + { + mAnimation->play(mCurrentWeapon, Priority_Weapon, + MWRender::Animation::Group_All, true, + 1, startKey, stopKey, + 0.0f, 0); + mUpperBodyState = UpperCharState_StartToMinAttack; + } } + + stats.setAttackingOrSpell(false); } bool animPlaying = mAnimation->getInfo(mCurrentWeapon); @@ -694,24 +857,57 @@ bool CharacterController::updateCreatureState() return false; } +bool CharacterController::updateCarriedLeftVisible(WeaponType weaptype) const +{ + // Shields/torches shouldn't be visible during any operation involving two hands + // There seems to be no text keys for this purpose, except maybe for "[un]equip start/stop", + // but they are also present in weapon drawing animation. + switch (weaptype) + { + case WeapType_Spell: + case WeapType_BowAndArrow: + case WeapType_Crossbow: + case WeapType_HandToHand: + case WeapType_TwoHand: + case WeapType_TwoWide: + return false; + default: + return true; + } +} + bool CharacterController::updateWeaponState() { const MWWorld::Class &cls = mPtr.getClass(); CreatureStats &stats = cls.getCreatureStats(mPtr); WeaponType weaptype = WeapType_None; - MWWorld::InventoryStore &inv = cls.getInventoryStore(mPtr); - MWWorld::ContainerStoreIterator weapon = getActiveWeapon(stats, inv, &weaptype); + if(stats.getDrawState() == DrawState_Weapon) + weaptype = WeapType_HandToHand; + else if (stats.getDrawState() == DrawState_Spell) + weaptype = WeapType_Spell; + const bool isWerewolf = cls.isNpc() && cls.getNpcStats(mPtr).isWerewolf(); + std::string soundid; + if (mPtr.getClass().hasInventoryStore(mPtr)) + { + MWWorld::InventoryStore &inv = cls.getInventoryStore(mPtr); + MWWorld::ContainerStoreIterator weapon = getActiveWeapon(stats, inv, &weaptype); + if(weapon != inv.end() && !(weaptype == WeapType_None && mWeaponType == WeapType_Spell)) + { + soundid = (weaptype == WeapType_None) ? + weapon->getClass().getDownSoundId(*weapon) : + weapon->getClass().getUpSoundId(*weapon); + } + } + bool forcestateupdate = false; - if(weaptype != mWeaponType && mHitState != CharState_KnockDown) + if(weaptype != mWeaponType && mHitState != CharState_KnockDown && mHitState != CharState_KnockOut + && mHitState != CharState_Hit) { forcestateupdate = true; - // Shields/torches shouldn't be visible during spellcasting or hand-to-hand - // There seems to be no text keys for this purpose, except maybe for "[un]equip start/stop", - // but they are also present in weapon drawing animation. - mAnimation->showCarriedLeft(weaptype != WeapType_Spell && weaptype != WeapType_HandToHand); + mAnimation->showCarriedLeft(updateCarriedLeftVisible(weaptype)); std::string weapgroup; if(weaptype == WeapType_None) @@ -745,16 +941,10 @@ bool CharacterController::updateWeaponState() } } - if(weapon != inv.end() && !(weaptype == WeapType_None && mWeaponType == WeapType_Spell)) + if(!soundid.empty()) { - std::string soundid = (weaptype == WeapType_None) ? - weapon->getClass().getDownSoundId(*weapon) : - weapon->getClass().getUpSoundId(*weapon); - if(!soundid.empty()) - { - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - sndMgr->playSound3D(mPtr, soundid, 1.0f, 1.0f); - } + MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); + sndMgr->playSound3D(mPtr, soundid, 1.0f, 1.0f); } mWeaponType = weaptype; @@ -765,6 +955,7 @@ bool CharacterController::updateWeaponState() { MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); if(cls.getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Run) + && mHasMovedInXY && !MWBase::Environment::get().getWorld()->isSwimming(mPtr) && mWeaponType == WeapType_None) { @@ -776,22 +967,28 @@ bool CharacterController::updateWeaponState() sndMgr->stopSound3D(mPtr, "WolfRun"); } - bool isWeapon = (weapon != inv.end() && weapon->getTypeName() == typeid(ESM::Weapon).name()); - float weapSpeed = 1.0f; - if(isWeapon) - weapSpeed = weapon->get()->mBase->mData.mSpeed; - // Cancel attack if we no longer have ammunition bool ammunition = true; - MWWorld::ContainerStoreIterator ammo = inv.getSlot(MWWorld::InventoryStore::Slot_Ammunition); - if (mWeaponType == WeapType_Crossbow) - ammunition = (ammo != inv.end() && ammo->get()->mBase->mData.mType == ESM::Weapon::Bolt); - else if (mWeaponType == WeapType_BowAndArrow) - ammunition = (ammo != inv.end() && ammo->get()->mBase->mData.mType == ESM::Weapon::Arrow); - if (!ammunition && mUpperBodyState > UpperCharState_WeapEquiped) + bool isWeapon = false; + float weapSpeed = 1.f; + if (mPtr.getClass().hasInventoryStore(mPtr)) { - mAnimation->disable(mCurrentWeapon); - mUpperBodyState = UpperCharState_WeapEquiped; + MWWorld::InventoryStore &inv = cls.getInventoryStore(mPtr); + MWWorld::ContainerStoreIterator weapon = getActiveWeapon(stats, inv, &weaptype); + isWeapon = (weapon != inv.end() && weapon->getTypeName() == typeid(ESM::Weapon).name()); + if(isWeapon) + weapSpeed = weapon->get()->mBase->mData.mSpeed; + + MWWorld::ContainerStoreIterator ammo = inv.getSlot(MWWorld::InventoryStore::Slot_Ammunition); + if (mWeaponType == WeapType_Crossbow) + ammunition = (ammo != inv.end() && ammo->get()->mBase->mData.mType == ESM::Weapon::Bolt); + else if (mWeaponType == WeapType_BowAndArrow) + ammunition = (ammo != inv.end() && ammo->get()->mBase->mData.mType == ESM::Weapon::Arrow); + if (!ammunition && mUpperBodyState > UpperCharState_WeapEquiped) + { + mAnimation->disable(mCurrentWeapon); + mUpperBodyState = UpperCharState_WeapEquiped; + } } float complete; @@ -822,9 +1019,7 @@ bool CharacterController::updateWeaponState() if(!spellid.empty() && MWBase::Environment::get().getWorld()->startSpellCast(mPtr)) { - static const std::string schools[] = { - "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration" - }; + castSpell(spellid); const ESM::Spell *spell = store.get().find(spellid); const ESM::ENAMstruct &effectentry = spell->mEffects.mList.at(0); @@ -832,19 +1027,16 @@ bool CharacterController::updateWeaponState() const ESM::MagicEffect *effect; effect = store.get().find(effectentry.mEffectID); - const ESM::Static* castStatic; - if (!effect->mCasting.empty()) - castStatic = store.get().find (effect->mCasting); + const ESM::Static* castStatic = MWBase::Environment::get().getWorld()->getStore().get().find ("VFX_Hands"); + if (mAnimation->getNode("Left Hand")) + mAnimation->addEffect("meshes\\" + castStatic->mModel, -1, false, "Left Hand", effect->mParticle); else - castStatic = store.get().find ("VFX_DefaultCast"); - - mAnimation->addEffect("meshes\\" + castStatic->mModel, effect->mIndex); + mAnimation->addEffect("meshes\\" + castStatic->mModel, -1, false, "Bip01 L Hand", effect->mParticle); - castStatic = MWBase::Environment::get().getWorld()->getStore().get().find ("VFX_Hands"); - //mAnimation->addEffect("meshes\\" + castStatic->mModel, -1, false, "Bip01 L Hand", effect->mParticle); - //mAnimation->addEffect("meshes\\" + castStatic->mModel, -1, false, "Bip01 R Hand", effect->mParticle); - mAnimation->addEffect("meshes\\" + castStatic->mModel, -1, false, "Left Hand", effect->mParticle); - mAnimation->addEffect("meshes\\" + castStatic->mModel, -1, false, "Right Hand", effect->mParticle); + if (mAnimation->getNode("Right Hand")) + mAnimation->addEffect("meshes\\" + castStatic->mModel, -1, false, "Right Hand", effect->mParticle); + else + mAnimation->addEffect("meshes\\" + castStatic->mModel, -1, false, "Bip01 R Hand", effect->mParticle); switch(effectentry.mRange) { @@ -858,22 +1050,23 @@ bool CharacterController::updateWeaponState() weapSpeed, mAttackType+" start", mAttackType+" stop", 0.0f, 0); mUpperBodyState = UpperCharState_CastingSpell; - - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - if(!effect->mCastSound.empty()) - sndMgr->playSound3D(mPtr, effect->mCastSound, 1.0f, 1.0f); - else - sndMgr->playSound3D(mPtr, schools[effect->mData.mSchool]+" cast", 1.0f, 1.0f); } - if (inv.getSelectedEnchantItem() != inv.end()) + if (mPtr.getClass().hasInventoryStore(mPtr)) { - // Enchanted items cast immediately (no animation) - MWBase::Environment::get().getWorld()->castSpell(mPtr); + MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); + if (inv.getSelectedEnchantItem() != inv.end()) + { + // Enchanted items cast immediately (no animation) + MWBase::Environment::get().getWorld()->castSpell(mPtr); + } } + } else if(mWeaponType == WeapType_PickProbe) { + MWWorld::ContainerStoreIterator weapon = mPtr.getClass().getInventoryStore(mPtr).getSlot(MWWorld::InventoryStore::Slot_CarriedRight); MWWorld::Ptr item = *weapon; + // TODO: this will only work for the player, and needs to be fixed if NPCs should ever use lockpicks/probes. MWWorld::Ptr target = MWBase::Environment::get().getWorld()->getFacedObject(); std::string resultMessage, resultSound; @@ -903,7 +1096,10 @@ bool CharacterController::updateWeaponState() { if(isWeapon && mPtr.getRefData().getHandle() == "player" && Settings::Manager::getBool("best attack", "Game")) + { + MWWorld::ContainerStoreIterator weapon = mPtr.getClass().getInventoryStore(mPtr).getSlot(MWWorld::InventoryStore::Slot_CarriedRight); mAttackType = getBestAttack(weapon->get()->mBase); + } else determineAttackType(); } @@ -923,7 +1119,16 @@ bool CharacterController::updateWeaponState() animPlaying = mAnimation->getInfo(mCurrentWeapon, &complete); if(mUpperBodyState == UpperCharState_MinAttackToMaxAttack && mHitState != CharState_KnockDown) { - if(mAttackType != "shoot") + float attackStrength = complete; + if (!mPtr.getClass().isNpc()) + { + // most creatures don't actually have an attack wind-up animation, so use a uniform random value + // (even some creatures that can use weapons don't have a wind-up animation either, e.g. Rieklings) + // Note: vanilla MW uses a random value for *all* non-player actors, but we probably don't need to go that far. + attackStrength = std::min(1.f, 0.1f + std::rand() / float(RAND_MAX)); + } + + if(mWeaponType != WeapType_Crossbow && mWeaponType != WeapType_BowAndArrow) { MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); @@ -937,15 +1142,15 @@ bool CharacterController::updateWeaponState() else { std::string sound = "SwishM"; - if(complete < 0.5f) + if(attackStrength < 0.5f) sndMgr->playSound3D(mPtr, sound, 1.0f, 0.8f); //Weak attack - else if(complete < 1.0f) + else if(attackStrength < 1.0f) sndMgr->playSound3D(mPtr, sound, 1.0f, 1.0f); //Medium attack else sndMgr->playSound3D(mPtr, sound, 1.0f, 1.2f); //Strong attack } } - stats.setAttackStrength(complete); + stats.setAttackStrength(attackStrength); mAnimation->disable(mCurrentWeapon); mAnimation->play(mCurrentWeapon, Priority_Weapon, @@ -958,7 +1163,8 @@ bool CharacterController::updateWeaponState() } else if (mHitState == CharState_KnockDown) { - mUpperBodyState = UpperCharState_WeapEquiped; + if (mUpperBodyState > UpperCharState_WeapEquiped) + mUpperBodyState = UpperCharState_WeapEquiped; mAnimation->disable(mCurrentWeapon); } } @@ -1113,17 +1319,21 @@ bool CharacterController::updateWeaponState() } } - MWWorld::ContainerStoreIterator torch = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); - if(torch != inv.end() && torch->getTypeName() == typeid(ESM::Light).name() - && mWeaponType != WeapType_Spell && mWeaponType != WeapType_HandToHand) - - { - mAnimation->play("torch", Priority_Torch, MWRender::Animation::Group_LeftArm, - false, 1.0f, "start", "stop", 0.0f, (~(size_t)0)); - } - else if (mAnimation->isPlaying("torch")) + if (mPtr.getClass().hasInventoryStore(mPtr)) { - mAnimation->disable("torch"); + MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); + MWWorld::ContainerStoreIterator torch = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); + if(torch != inv.end() && torch->getTypeName() == typeid(ESM::Light).name() + && updateCarriedLeftVisible(mWeaponType)) + + { + mAnimation->play("torch", Priority_Torch, MWRender::Animation::Group_LeftArm, + false, 1.0f, "start", "stop", 0.0f, (~(size_t)0), true); + } + else if (mAnimation->isPlaying("torch")) + { + mAnimation->disable("torch"); + } } return forcestateupdate; @@ -1135,7 +1345,7 @@ void CharacterController::update(float duration) const MWWorld::Class &cls = mPtr.getClass(); Ogre::Vector3 movement(0.0f); - updateVisibility(); + updateMagicEffects(); if(!cls.isActor()) { @@ -1156,20 +1366,42 @@ void CharacterController::update(float duration) { bool onground = world->isOnGround(mPtr); bool inwater = world->isSwimming(mPtr); - bool isrunning = cls.getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Run); bool sneak = cls.getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Sneak); bool flying = world->isFlying(mPtr); - //Ogre::Vector3 vec = cls.getMovementVector(mPtr); - Ogre::Vector3 vec(cls.getMovementSettings(mPtr).mPosition); - if(vec.z > 0.0f) // to avoid slow-down when jumping + // Can't run while flying (see speed formula in Npc/Creature::getSpeed) + bool isrunning = cls.getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Run) && !flying; + CreatureStats &stats = cls.getCreatureStats(mPtr); + + //Force Jump Logic + + bool isMoving = (std::abs(cls.getMovementSettings(mPtr).mPosition[0]) > .5 || std::abs(cls.getMovementSettings(mPtr).mPosition[1]) > .5); + if(!inwater && !flying) { - Ogre::Vector2 vecXY = Ogre::Vector2(vec.x, vec.y); - vecXY.normalise(); - vec.x = vecXY.x; - vec.y = vecXY.y; + //Force Jump + if(stats.getMovementFlag(MWMechanics::CreatureStats::Flag_ForceJump)) + { + if(onground) + { + cls.getMovementSettings(mPtr).mPosition[2] = 1; + } + else + cls.getMovementSettings(mPtr).mPosition[2] = 0; + } + //Force Move Jump, only jump if they're otherwise moving + if(stats.getMovementFlag(MWMechanics::CreatureStats::Flag_ForceMoveJump) && isMoving) + { + + if(onground) + { + cls.getMovementSettings(mPtr).mPosition[2] = 1; + } + else + cls.getMovementSettings(mPtr).mPosition[2] = 0; + } } - else - vec.normalise(); + + Ogre::Vector3 vec(cls.getMovementSettings(mPtr).mPosition); + vec.normalise(); if(mHitState != CharState_None && mJumpState == JumpState_None) vec = Ogre::Vector3(0.0f); @@ -1184,11 +1416,12 @@ void CharacterController::update(float duration) CharacterState idlestate = CharState_SpecialIdle; bool forcestateupdate = false; - isrunning = isrunning && std::abs(vec[0])+std::abs(vec[1]) > 0.0f; + mHasMovedInXY = std::abs(vec[0])+std::abs(vec[1]) > 0.0f; + isrunning = isrunning && mHasMovedInXY; // advance athletics - if(std::abs(vec[0])+std::abs(vec[1]) > 0.0f && mPtr.getRefData().getHandle() == "player") + if(mHasMovedInXY && mPtr.getRefData().getHandle() == "player") { if(inwater) { @@ -1262,55 +1495,56 @@ void CharacterController::update(float duration) cls.getCreatureStats(mPtr).land(); } - forcestateupdate = (mJumpState != JumpState_Falling); - mJumpState = JumpState_Falling; - - // This is a guess. All that seems to be known is that "While the player is in the - // air, fJumpMoveBase and fJumpMoveMult governs air control." Assuming Acrobatics - // plays a role, this makes the most sense. - float mult = 0.0f; - if(cls.isNpc()) - { - const NpcStats &stats = cls.getNpcStats(mPtr); - static const float fJumpMoveBase = gmst.find("fJumpMoveBase")->getFloat(); - static const float fJumpMoveMult = gmst.find("fJumpMoveMult")->getFloat(); + forcestateupdate = (mJumpState != JumpState_InAir); + mJumpState = JumpState_InAir; - mult = fJumpMoveBase + - (stats.getSkill(ESM::Skill::Acrobatics).getModified()/100.0f * - fJumpMoveMult); - } - - vec.x *= mult; - vec.y *= mult; + static const float fJumpMoveBase = gmst.find("fJumpMoveBase")->getFloat(); + static const float fJumpMoveMult = gmst.find("fJumpMoveMult")->getFloat(); + float factor = fJumpMoveBase + fJumpMoveMult * mPtr.getClass().getSkill(mPtr, ESM::Skill::Acrobatics)/100.f; + factor = std::min(1.f, factor); + vec.x *= factor; + vec.y *= factor; vec.z = 0.0f; } else if(vec.z > 0.0f && mJumpState == JumpState_None) { // Started a jump. - vec.z = cls.getJump(mPtr); - - // advance acrobatics - if (mPtr.getRefData().getHandle() == "player") - cls.skillUsageSucceeded(mPtr, ESM::Skill::Acrobatics, 0); - - // decrease fatigue - const MWWorld::Store &gmst = world->getStore().get(); - const float fatigueJumpBase = gmst.find("fFatigueJumpBase")->getFloat(); - const float fatigueJumpMult = gmst.find("fFatigueJumpMult")->getFloat(); - const float normalizedEncumbrance = cls.getEncumbrance(mPtr) / cls.getCapacity(mPtr); - const int fatigueDecrease = fatigueJumpBase + (1 - normalizedEncumbrance) * fatigueJumpMult; - DynamicStat fatigue = cls.getCreatureStats(mPtr).getFatigue(); - fatigue.setCurrent(fatigue.getCurrent() - fatigueDecrease); - cls.getCreatureStats(mPtr).setFatigue(fatigue); + float z = cls.getJump(mPtr); + if (z > 0) + { + if(vec.x == 0 && vec.y == 0) + vec = Ogre::Vector3(0.0f, 0.0f, z); + else + { + Ogre::Vector3 lat = Ogre::Vector3(vec.x, vec.y, 0.0f).normalisedCopy(); + vec = Ogre::Vector3(lat.x, lat.y, 1.0f) * z * 0.707f; + } + + // advance acrobatics + if (mPtr.getRefData().getHandle() == "player") + cls.skillUsageSucceeded(mPtr, ESM::Skill::Acrobatics, 0); + + // decrease fatigue + const MWWorld::Store &gmst = world->getStore().get(); + const float fatigueJumpBase = gmst.find("fFatigueJumpBase")->getFloat(); + const float fatigueJumpMult = gmst.find("fFatigueJumpMult")->getFloat(); + float normalizedEncumbrance = mPtr.getClass().getNormalizedEncumbrance(mPtr); + if (normalizedEncumbrance > 1) + normalizedEncumbrance = 1; + const int fatigueDecrease = fatigueJumpBase + (1 - normalizedEncumbrance) * fatigueJumpMult; + DynamicStat fatigue = cls.getCreatureStats(mPtr).getFatigue(); + fatigue.setCurrent(fatigue.getCurrent() - fatigueDecrease); + cls.getCreatureStats(mPtr).setFatigue(fatigue); + } } - else if(mJumpState == JumpState_Falling) + else if(mJumpState == JumpState_InAir) { forcestateupdate = true; mJumpState = JumpState_Landing; vec.z = 0.0f; float height = cls.getCreatureStats(mPtr).land(); - float healthLost = cls.getFallDamage(mPtr, height); + float healthLost = getFallDamage(mPtr, height); if (healthLost > 0.0f) { const float fatigueTerm = cls.getCreatureStats(mPtr).getFatigueTerm(); @@ -1337,8 +1571,7 @@ void CharacterController::update(float duration) } else { - if(!(vec.z > 0.0f)) - mJumpState = JumpState_None; + mJumpState = JumpState_None; vec.z = 0.0f; inJump = false; @@ -1377,6 +1610,15 @@ void CharacterController::update(float duration) } } + mTurnAnimationThreshold -= duration; + if (movestate == CharState_TurnRight || movestate == CharState_TurnLeft) + mTurnAnimationThreshold = 0.05; + else if (movestate == CharState_None && (mMovementState == CharState_TurnRight || mMovementState == CharState_TurnLeft) + && mTurnAnimationThreshold > 0) + { + movestate = mMovementState; + } + if (onground) cls.getCreatureStats(mPtr).land(); @@ -1384,7 +1626,9 @@ void CharacterController::update(float duration) clearAnimQueue(); if(mAnimQueue.empty()) + { idlestate = (inwater ? CharState_IdleSwim : (sneak ? CharState_IdleSneak : CharState_Idle)); + } else if(mAnimQueue.size() > 1) { if(mAnimation->isPlaying(mAnimQueue.front().first) == false) @@ -1398,15 +1642,22 @@ void CharacterController::update(float duration) } } - if(cls.hasInventoryStore(mPtr)) + if(cls.isBipedal(mPtr)) forcestateupdate = updateWeaponState() || forcestateupdate; else forcestateupdate = updateCreatureState() || forcestateupdate; - refreshCurrentAnims(idlestate, movestate, forcestateupdate); + if (!mSkipAnim) + refreshCurrentAnims(idlestate, movestate, forcestateupdate); if (inJump) mMovementAnimationControlled = false; + if (mMovementState == CharState_TurnLeft || mMovementState == CharState_TurnRight) + { + if (duration > 0) + mAnimation->adjustSpeedMult(mCurrentMovement, std::min(1.5f, std::abs(rot.z) / duration / Ogre::Math::PI)); + } + if (!mSkipAnim) { rot *= Ogre::Math::RadiansToDegrees(1.0f); @@ -1425,48 +1676,54 @@ void CharacterController::update(float duration) world->queueMovement(mPtr, Ogre::Vector3(0.0f)); movement = vec; - cls.getMovementSettings(mPtr).mPosition[0] = cls.getMovementSettings(mPtr).mPosition[1] = cls.getMovementSettings(mPtr).mPosition[2] = 0; + cls.getMovementSettings(mPtr).mPosition[0] = cls.getMovementSettings(mPtr).mPosition[1] = 0; + // Can't reset jump state (mPosition[2]) here; we don't know for sure whether the PhysicSystem will actually handle it in this frame + // due to the fixed minimum timestep used for the physics update. It will be reset in PhysicSystem::move once the jump is handled. + + if (!mSkipAnim) + updateHeadTracking(duration); } else if(cls.getCreatureStats(mPtr).isDead()) { world->queueMovement(mPtr, Ogre::Vector3(0.0f)); } - if(mAnimation && !mSkipAnim) - { - Ogre::Vector3 moved = mAnimation->runAnimation(duration); - if(duration > 0.0f) - moved /= duration; - else - moved = Ogre::Vector3(0.0f); - - // Ensure we're moving in generally the right direction... - if(mMovementSpeed > 0.f) - { - float l = moved.length(); - - if((movement.x < 0.0f && movement.x < moved.x*2.0f) || - (movement.x > 0.0f && movement.x > moved.x*2.0f)) - moved.x = movement.x; - if((movement.y < 0.0f && movement.y < moved.y*2.0f) || - (movement.y > 0.0f && movement.y > moved.y*2.0f)) - moved.y = movement.y; - if((movement.z < 0.0f && movement.z < moved.z*2.0f) || - (movement.z > 0.0f && movement.z > moved.z*2.0f)) - moved.z = movement.z; - // but keep the original speed - float newLength = moved.length(); - if (newLength > 0) - moved *= (l / newLength); - } + Ogre::Vector3 moved = mAnimation->runAnimation(mSkipAnim ? 0.f : duration); + if(duration > 0.0f) + moved /= duration; + else + moved = Ogre::Vector3(0.0f); - // Update movement - if(mMovementAnimationControlled && mPtr.getClass().isActor()) - world->queueMovement(mPtr, moved); + // Ensure we're moving in generally the right direction... + if(mMovementSpeed > 0.f) + { + float l = moved.length(); + + if((movement.x < 0.0f && movement.x < moved.x*2.0f) || + (movement.x > 0.0f && movement.x > moved.x*2.0f)) + moved.x = movement.x; + if((movement.y < 0.0f && movement.y < moved.y*2.0f) || + (movement.y > 0.0f && movement.y > moved.y*2.0f)) + moved.y = movement.y; + if((movement.z < 0.0f && movement.z < moved.z*2.0f) || + (movement.z > 0.0f && movement.z > moved.z*2.0f)) + moved.z = movement.z; + // but keep the original speed + float newLength = moved.length(); + if (newLength > 0) + moved *= (l / newLength); } - else if (mAnimation) + + if (mSkipAnim) mAnimation->updateEffects(duration); + + // Update movement + if(mMovementAnimationControlled && mPtr.getClass().isActor()) + world->queueMovement(mPtr, moved); + mSkipAnim = false; + + mAnimation->enableHeadAnimation(cls.isActor() && !cls.getCreatureStats(mPtr).isDead()); } @@ -1492,6 +1749,8 @@ void CharacterController::playGroup(const std::string &groupname, int mode, int } else if(mode == 0) { + if (!mAnimQueue.empty()) + mAnimation->stopLooping(mAnimQueue.front().first); mAnimQueue.resize(1); mAnimQueue.push_back(std::make_pair(groupname, count-1)); } @@ -1530,6 +1789,8 @@ void CharacterController::forceStateUpdate() { playRandomDeath(); } + + mAnimation->runAnimation(0.f); } bool CharacterController::kill() @@ -1546,14 +1807,15 @@ bool CharacterController::kill() playRandomDeath(); - if(mAnimation) - { - mAnimation->disable(mCurrentIdle); - } + mAnimation->disable(mCurrentIdle); mIdleState = CharState_None; mCurrentIdle.clear(); + // Play Death Music if it was the player dying + if(mPtr.getRefData().getHandle()=="player") + MWBase::Environment::get().getSoundManager()->streamMusic("Special/MW_Death.mp3"); + return true; } @@ -1580,45 +1842,108 @@ void CharacterController::updateContinuousVfx() for (std::vector::iterator it = effects.begin(); it != effects.end(); ++it) { if (mPtr.getClass().getCreatureStats(mPtr).isDead() - || mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(MWMechanics::EffectKey(*it)).mMagnitude <= 0) + || mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(MWMechanics::EffectKey(*it)).getMagnitude() <= 0) mAnimation->removeEffect(*it); } } -void CharacterController::updateVisibility() +void CharacterController::updateMagicEffects() { if (!mPtr.getClass().isActor()) return; float alpha = 1.f; - if (mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Invisibility).mMagnitude) + if (mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Invisibility).getMagnitude()) { if (mPtr.getRefData().getHandle() == "player") alpha = 0.4f; else alpha = 0.f; } - float chameleon = mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Chameleon).mMagnitude; + float chameleon = mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Chameleon).getMagnitude(); if (chameleon) { alpha *= std::max(0.2f, (100.f - chameleon)/100.f); } - mAnimation->setAlpha(alpha); + + bool vampire = mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Vampirism).getMagnitude() > 0.0f; + mAnimation->setVampire(vampire); + + float light = mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Light).getMagnitude(); + mAnimation->setLightEffect(light); } void CharacterController::determineAttackType() { float *move = mPtr.getClass().getMovementSettings(mPtr).mPosition; - if(mPtr.getClass().hasInventoryStore(mPtr)) + if (move[1] && !move[0]) // forward-backward + mAttackType = "thrust"; + else if (move[0] && !move[1]) //sideway + mAttackType = "slash"; + else + mAttackType = "chop"; +} + +bool CharacterController::isReadyToBlock() const +{ + return updateCarriedLeftVisible(mWeaponType); +} + +bool CharacterController::isKnockedOut() const +{ + return mHitState == CharState_KnockOut; +} + +void CharacterController::setHeadTrackTarget(const MWWorld::Ptr &target) +{ + mHeadTrackTarget = target; +} + +void CharacterController::updateHeadTracking(float duration) +{ + Ogre::Node* head = mAnimation->getNode("Bip01 Head"); + if (!head) + return; + Ogre::Radian zAngle (0.f); + Ogre::Radian xAngle (0.f); + if (!mHeadTrackTarget.isEmpty()) { - if (move[1]) // forward-backward - mAttackType = "thrust"; - else if (move[0]) //sideway - mAttackType = "slash"; - else - mAttackType = "chop"; + Ogre::Vector3 headPos = mPtr.getRefData().getBaseNode()->convertLocalToWorldPosition(head->_getDerivedPosition()); + Ogre::Vector3 targetPos (mHeadTrackTarget.getRefData().getPosition().pos); + if (MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(mHeadTrackTarget)) + { + Ogre::Node* targetHead = anim->getNode("Head"); + if (!targetHead) + targetHead = anim->getNode("Bip01 Head"); + if (targetHead) + targetPos = mHeadTrackTarget.getRefData().getBaseNode()->convertLocalToWorldPosition( + targetHead->_getDerivedPosition()); + } + + Ogre::Vector3 direction = targetPos - headPos; + direction.normalise(); + + const Ogre::Vector3 actorDirection = mPtr.getRefData().getBaseNode()->getOrientation().yAxis(); + + zAngle = Ogre::Math::ATan2(direction.x,direction.y) - + Ogre::Math::ATan2(actorDirection.x, actorDirection.y); + xAngle = -Ogre::Math::ASin(direction.z); + wrap(zAngle); + wrap(xAngle); + xAngle = Ogre::Degree(std::min(xAngle.valueDegrees(), 40.f)); + xAngle = Ogre::Degree(std::max(xAngle.valueDegrees(), -40.f)); + zAngle = Ogre::Degree(std::min(zAngle.valueDegrees(), 30.f)); + zAngle = Ogre::Degree(std::max(zAngle.valueDegrees(), -30.f)); + } + float factor = duration*5; + factor = std::min(factor, 1.f); + xAngle = (1.f-factor) * mAnimation->getHeadPitch() + factor * (-xAngle); + zAngle = (1.f-factor) * mAnimation->getHeadYaw() + factor * (-zAngle); + + mAnimation->setHeadPitch(xAngle); + mAnimation->setHeadYaw(zAngle); } } diff --git a/apps/openmw/mwmechanics/character.hpp b/apps/openmw/mwmechanics/character.hpp index b1e1738bd..8a77494b9 100644 --- a/apps/openmw/mwmechanics/character.hpp +++ b/apps/openmw/mwmechanics/character.hpp @@ -32,6 +32,7 @@ enum Priority { Priority_Weapon, Priority_Knockdown, Priority_Torch, + Priority_Storm, Priority_Death, @@ -129,7 +130,7 @@ enum UpperBodyCharacterState { enum JumpingState { JumpState_None, - JumpState_Falling, + JumpState_InAir, JumpState_Landing }; @@ -137,7 +138,7 @@ class CharacterController { MWWorld::Ptr mPtr; MWRender::Animation *mAnimation; - + typedef std::deque > AnimationQueue; AnimationQueue mAnimQueue; @@ -147,6 +148,7 @@ class CharacterController CharacterState mMovementState; std::string mCurrentMovement; float mMovementSpeed; + bool mHasMovedInXY; bool mMovementAnimationControlled; CharacterState mDeathState; @@ -169,6 +171,10 @@ class CharacterController float mSecondsOfSwimming; float mSecondsOfRunning; + MWWorld::Ptr mHeadTrackTarget; + + float mTurnAnimationThreshold; // how long to continue playing turning animation after actor stopped turning + std::string mAttackType; // slash, chop or thrust void determineAttackType(); @@ -180,7 +186,11 @@ class CharacterController bool updateCreatureState(); void updateIdleStormState(); - void updateVisibility(); + void updateHeadTracking(float duration); + + void castSpell(const std::string& spellid); + + void updateMagicEffects(); void playDeath(float startpoint, CharacterState death); void playRandomDeath(float startpoint = 0.0f); @@ -189,6 +199,8 @@ class CharacterController /// @param num if non-NULL, the chosen animation number will be written here std::string chooseRandomGroup (const std::string& prefix, int* num = NULL); + bool updateCarriedLeftVisible(WeaponType weaptype) const; + public: CharacterController(const MWWorld::Ptr &ptr, MWRender::Animation *anim); virtual ~CharacterController(); @@ -212,6 +224,12 @@ public: { return mDeathState != CharState_None; } void forceStateUpdate(); + + bool isReadyToBlock() const; + bool isKnockedOut() const; + + /// Make this character turn its head towards \a target. To turn off head tracking, pass an empty Ptr. + void setHeadTrackTarget(const MWWorld::Ptr& target); }; void getWeaponGroup(WeaponType weaptype, std::string &group); diff --git a/apps/openmw/mwmechanics/combat.cpp b/apps/openmw/mwmechanics/combat.cpp index c13eac98c..e22e9ec24 100644 --- a/apps/openmw/mwmechanics/combat.cpp +++ b/apps/openmw/mwmechanics/combat.cpp @@ -29,7 +29,7 @@ Ogre::Radian signedAngle(Ogre::Vector3 v1, Ogre::Vector3 v2, Ogre::Vector3 norma ); } -void applyEnchantment (const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim, const MWWorld::Ptr& object, const Ogre::Vector3& hitPosition) +bool applyEnchantment (const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim, const MWWorld::Ptr& object, const Ogre::Vector3& hitPosition) { std::string enchantmentName = !object.isEmpty() ? object.getClass().getEnchantment(object) : ""; if (!enchantmentName.empty()) @@ -41,8 +41,10 @@ void applyEnchantment (const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim, MWMechanics::CastSpell cast(attacker, victim); cast.mHitPosition = hitPosition; cast.cast(object); + return true; } } + return false; } } @@ -59,20 +61,13 @@ namespace MWMechanics if (blockerStats.getKnockedDown() // Used for both knockout or knockdown || blockerStats.getHitRecovery() - || blockerStats.getMagicEffects().get(ESM::MagicEffect::Paralyze).mMagnitude > 0) + || blockerStats.getMagicEffects().get(ESM::MagicEffect::Paralyze).getMagnitude() > 0) return false; - // Don't block when in spellcasting state (shield is equipped, but not visible) - if (blockerStats.getDrawState() == DrawState_Spell) + if (!MWBase::Environment::get().getMechanicsManager()->isReadyToBlock(blocker)) return false; MWWorld::InventoryStore& inv = blocker.getClass().getInventoryStore(blocker); - - // Don't block when in hand-to-hand combat (shield is equipped, but not visible) - if (blockerStats.getDrawState() == DrawState_Weapon && - inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight) == inv.end()) - return false; - MWWorld::ContainerStoreIterator shield = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); if (shield == inv.end() || shield->getTypeName() != typeid(ESM::Armor).name()) return false; @@ -98,7 +93,11 @@ namespace MWMechanics blockerTerm *= gmst.find("fBlockStillBonus")->getFloat(); blockerTerm *= blockerStats.getFatigueTerm(); - float attackerSkill = attacker.getClass().getSkill(attacker, weapon.getClass().getEquipmentSkill(weapon)); + float attackerSkill = 0.f; + if (weapon.isEmpty()) + attackerSkill = attacker.getClass().getSkill(attacker, ESM::Skill::HandToHand); + else + attackerSkill = attacker.getClass().getSkill(attacker, weapon.getClass().getEquipmentSkill(weapon)); float attackerTerm = attackerSkill + 0.2 * attackerStats.getAttribute(ESM::Attribute::Agility).getModified() + 0.1 * attackerStats.getAttribute(ESM::Attribute::Luck).getModified(); attackerTerm *= attackerStats.getFatigueTerm(); @@ -124,16 +123,17 @@ namespace MWMechanics const float fFatigueBlockMult = gmst.find("fFatigueBlockMult")->getFloat(); const float fWeaponFatigueBlockMult = gmst.find("fWeaponFatigueBlockMult")->getFloat(); MWMechanics::DynamicStat fatigue = blockerStats.getFatigue(); - float normalizedEncumbrance = blocker.getClass().getEncumbrance(blocker) / blocker.getClass().getCapacity(blocker); + float normalizedEncumbrance = blocker.getClass().getNormalizedEncumbrance(blocker); normalizedEncumbrance = std::min(1.f, normalizedEncumbrance); float fatigueLoss = fFatigueBlockBase + normalizedEncumbrance * fFatigueBlockMult; - fatigueLoss += weapon.getClass().getWeight(weapon) * attackerStats.getAttackStrength() * fWeaponFatigueBlockMult; + if (!weapon.isEmpty()) + fatigueLoss += weapon.getClass().getWeight(weapon) * attackerStats.getAttackStrength() * fWeaponFatigueBlockMult; fatigue.setCurrent(fatigue.getCurrent() - fatigueLoss); blockerStats.setFatigue(fatigue); blockerStats.setBlock(true); - if (blocker.getClass().isNpc()) + if (blocker.getCellRef().getRefId() == "player") blocker.getClass().skillUsageSucceeded(blocker, ESM::Skill::Block, 0); return true; @@ -144,8 +144,8 @@ namespace MWMechanics void resistNormalWeapon(const MWWorld::Ptr &actor, const MWWorld::Ptr& attacker, const MWWorld::Ptr &weapon, float &damage) { MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor); - float resistance = std::min(100.f, stats.getMagicEffects().get(ESM::MagicEffect::ResistNormalWeapons).mMagnitude - - stats.getMagicEffects().get(ESM::MagicEffect::WeaknessToNormalWeapons).mMagnitude); + float resistance = std::min(100.f, stats.getMagicEffects().get(ESM::MagicEffect::ResistNormalWeapons).getMagnitude() + - stats.getMagicEffects().get(ESM::MagicEffect::WeaknessToNormalWeapons).getMagnitude()); float multiplier = 1.f - resistance / 100.f; @@ -169,12 +169,12 @@ namespace MWMechanics MWMechanics::CreatureStats& attackerStats = attacker.getClass().getCreatureStats(attacker); - const MWWorld::Class &othercls = victim.getClass(); - if(!othercls.isActor()) // Can't hit non-actors - return; - MWMechanics::CreatureStats &otherstats = victim.getClass().getCreatureStats(victim); - if(otherstats.isDead()) // Can't hit dead actors + if(victim.isEmpty() || !victim.getClass().isActor() || victim.getClass().getCreatureStats(victim).isDead()) + // Can't hit non-actors or dead actors + { + reduceWeaponCondition(0.f, false, weapon, attacker); return; + } if(attacker.getRefData().getHandle() == "player") MWBase::Environment::get().getWindowManager()->setEnemy(victim); @@ -189,16 +189,15 @@ namespace MWMechanics if((::rand()/(RAND_MAX+1.0)) > getHitChance(attacker, victim, skillValue)/100.0f) { victim.getClass().onHit(victim, 0.0f, false, projectile, attacker, false); + MWMechanics::reduceWeaponCondition(0.f, false, weapon, attacker); return; } - float damage = 0.0f; - float fDamageStrengthBase = gmst.find("fDamageStrengthBase")->getFloat(); float fDamageStrengthMult = gmst.find("fDamageStrengthMult")->getFloat(); const unsigned char* attack = weapon.get()->mBase->mData.mChop; - damage = attack[0] + ((attack[1]-attack[0])*attackerStats.getAttackStrength()); // Bow/crossbow damage + float damage = attack[0] + ((attack[1]-attack[0])*attackerStats.getAttackStrength()); // Bow/crossbow damage if (weapon != projectile) { // Arrow/bolt damage @@ -209,6 +208,8 @@ namespace MWMechanics damage *= fDamageStrengthBase + (attackerStats.getAttribute(ESM::Attribute::Strength).getModified() * fDamageStrengthMult * 0.1); + adjustWeaponDamage(damage, weapon); + reduceWeaponCondition(damage, true, weapon, attacker); if(attacker.getRefData().getHandle() == "player") attacker.getClass().skillUsageSucceeded(attacker, weapskill, 0); @@ -217,15 +218,16 @@ namespace MWMechanics damage *= gmst.find("fCombatKODamageMult")->getFloat(); // Apply "On hit" effect of the weapon - applyEnchantment(attacker, victim, weapon, hitPosition); + bool appliedEnchantment = applyEnchantment(attacker, victim, weapon, hitPosition); if (weapon != projectile) - applyEnchantment(attacker, victim, projectile, hitPosition); + appliedEnchantment = applyEnchantment(attacker, victim, projectile, hitPosition); if (damage > 0) MWBase::Environment::get().getWorld()->spawnBloodEffect(victim, hitPosition); - // Arrows shot at enemies have a chance to turn up in their inventory - if (victim != MWBase::Environment::get().getWorld()->getPlayerPtr()) + // Non-enchanted arrows shot at enemies have a chance to turn up in their inventory + if (victim != MWBase::Environment::get().getWorld()->getPlayerPtr() + && !appliedEnchantment) { float fProjectileThrownStoreChance = gmst.find("fProjectileThrownStoreChance")->getFloat(); if ((::rand()/(RAND_MAX+1.0)) < fProjectileThrownStoreChance/100.f) @@ -243,8 +245,8 @@ namespace MWMechanics (stats.getAttribute(ESM::Attribute::Agility).getModified() / 5.0f) + (stats.getAttribute(ESM::Attribute::Luck).getModified() / 10.0f); hitchance *= stats.getFatigueTerm(); - hitchance += mageffects.get(ESM::MagicEffect::FortifyAttack).mMagnitude - - mageffects.get(ESM::MagicEffect::Blind).mMagnitude; + hitchance += mageffects.get(ESM::MagicEffect::FortifyAttack).getMagnitude() - + mageffects.get(ESM::MagicEffect::Blind).getMagnitude(); hitchance -= victim.getClass().getCreatureStats(victim).getEvasion(); return hitchance; } @@ -253,7 +255,7 @@ namespace MWMechanics { for (int i=0; i<3; ++i) { - float magnitude = victim.getClass().getCreatureStats(victim).getMagicEffects().get(ESM::MagicEffect::FireShield+i).mMagnitude; + float magnitude = victim.getClass().getCreatureStats(victim).getMagicEffects().get(ESM::MagicEffect::FireShield+i).getMagnitude(); if (!magnitude) continue; @@ -295,4 +297,114 @@ namespace MWMechanics } } + void reduceWeaponCondition(float damage, bool hit, MWWorld::Ptr &weapon, const MWWorld::Ptr &attacker) + { + if (weapon.isEmpty()) + return; + + if (!hit) + damage = 0.f; + + const bool weaphashealth = weapon.getClass().hasItemHealth(weapon); + if(weaphashealth) + { + int weaphealth = weapon.getClass().getItemHealth(weapon); + + const float fWeaponDamageMult = MWBase::Environment::get().getWorld()->getStore().get().find("fWeaponDamageMult")->getFloat(); + float x = std::max(1.f, fWeaponDamageMult * damage); + + weaphealth -= std::min(int(x), weaphealth); + weapon.getCellRef().setCharge(weaphealth); + + // Weapon broken? unequip it + if (weaphealth == 0) + weapon = *attacker.getClass().getInventoryStore(attacker).unequipItem(weapon, attacker); + } + } + + void adjustWeaponDamage(float &damage, const MWWorld::Ptr &weapon) + { + if (weapon.isEmpty()) + return; + + const bool weaphashealth = weapon.getClass().hasItemHealth(weapon); + if(weaphashealth) + { + int weaphealth = weapon.getClass().getItemHealth(weapon); + int weapmaxhealth = weapon.getClass().getItemMaxHealth(weapon); + damage *= (float(weaphealth) / weapmaxhealth); + } + } + + void getHandToHandDamage(const MWWorld::Ptr &attacker, const MWWorld::Ptr &victim, float &damage, bool &healthdmg) + { + // Note: MCP contains an option to include Strength in hand-to-hand damage + // calculations. Some mods recommend using it, so we may want to include an + // option for it. + const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); + float minstrike = store.get().find("fMinHandToHandMult")->getFloat(); + float maxstrike = store.get().find("fMaxHandToHandMult")->getFloat(); + damage = attacker.getClass().getSkill(attacker, ESM::Skill::HandToHand); + damage *= minstrike + ((maxstrike-minstrike)*attacker.getClass().getCreatureStats(attacker).getAttackStrength()); + + MWMechanics::CreatureStats& otherstats = victim.getClass().getCreatureStats(victim); + healthdmg = (otherstats.getMagicEffects().get(ESM::MagicEffect::Paralyze).getMagnitude() > 0) + || otherstats.getKnockedDown(); + bool isWerewolf = (attacker.getClass().isNpc() && attacker.getClass().getNpcStats(attacker).isWerewolf()); + if(isWerewolf) + { + healthdmg = true; + // GLOB instead of GMST because it gets updated during a quest + damage *= MWBase::Environment::get().getWorld()->getGlobalFloat("werewolfclawmult"); + } + if(healthdmg) + damage *= store.get().find("fHandtoHandHealthPer")->getFloat(); + + MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); + if(isWerewolf) + { + const ESM::Sound *sound = store.get().searchRandom("WolfHit"); + if(sound) + sndMgr->playSound3D(victim, sound->mId, 1.0f, 1.0f); + } + else + sndMgr->playSound3D(victim, "Hand To Hand Hit", 1.0f, 1.0f); + } + + void applyFatigueLoss(const MWWorld::Ptr &attacker, const MWWorld::Ptr &weapon) + { + // somewhat of a guess, but using the weapon weight makes sense + const MWWorld::Store& store = MWBase::Environment::get().getWorld()->getStore().get(); + const float fFatigueAttackBase = store.find("fFatigueAttackBase")->getFloat(); + const float fFatigueAttackMult = store.find("fFatigueAttackMult")->getFloat(); + const float fWeaponFatigueMult = store.find("fWeaponFatigueMult")->getFloat(); + CreatureStats& stats = attacker.getClass().getCreatureStats(attacker); + MWMechanics::DynamicStat fatigue = stats.getFatigue(); + const float normalizedEncumbrance = attacker.getClass().getNormalizedEncumbrance(attacker); + float fatigueLoss = fFatigueAttackBase + normalizedEncumbrance * fFatigueAttackMult; + if (!weapon.isEmpty()) + fatigueLoss += weapon.getClass().getWeight(weapon) * stats.getAttackStrength() * fWeaponFatigueMult; + fatigue.setCurrent(fatigue.getCurrent() - fatigueLoss); + stats.setFatigue(fatigue); + } + + bool isEnvironmentCompatible(const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim) + { + const MWWorld::Class& attackerClass = attacker.getClass(); + MWBase::World* world = MWBase::Environment::get().getWorld(); + + // If attacker is fish, victim must be in water + if (attackerClass.isPureWaterCreature(attacker)) + { + return world->isWading(victim); + } + + // If attacker can't swim, victim must not be in water + if (!attackerClass.canSwim(attacker)) + { + return !world->isSwimming(victim); + } + + return true; + } } diff --git a/apps/openmw/mwmechanics/combat.hpp b/apps/openmw/mwmechanics/combat.hpp index eb89cb820..a48dcf72a 100644 --- a/apps/openmw/mwmechanics/combat.hpp +++ b/apps/openmw/mwmechanics/combat.hpp @@ -13,6 +13,7 @@ bool blockMeleeAttack (const MWWorld::Ptr& attacker, const MWWorld::Ptr& blocker void resistNormalWeapon (const MWWorld::Ptr& actor, const MWWorld::Ptr& attacker, const MWWorld::Ptr& weapon, float& damage); /// @note for a thrown weapon, \a weapon == \a projectile, for bows/crossbows, \a projectile is the arrow/bolt +/// @note \a victim may be empty (e.g. for a hit on terrain), a non-actor (environment objects) or an actor void projectileHit (const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim, MWWorld::Ptr weapon, const MWWorld::Ptr& projectile, const Ogre::Vector3& hitPosition); @@ -22,6 +23,24 @@ float getHitChance (const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim, in /// Applies damage to attacker based on the victim's elemental shields. void applyElementalShields(const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim); +/// @param damage Unmitigated weapon damage of the attack +/// @param hit Was the attack successful? +/// @param weapon The weapon used. +/// @note if the weapon is unequipped as result of condition damage, a new Ptr will be assigned to \a weapon. +void reduceWeaponCondition (float damage, bool hit, MWWorld::Ptr& weapon, const MWWorld::Ptr& attacker); + +/// Adjust weapon damage based on its condition. A used weapon will be less effective. +void adjustWeaponDamage (float& damage, const MWWorld::Ptr& weapon); + +void getHandToHandDamage (const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim, float& damage, bool& healthdmg); + +/// Apply the fatigue loss incurred by attacking with the given weapon (weapon may be empty = hand-to-hand) +void applyFatigueLoss(const MWWorld::Ptr& attacker, const MWWorld::Ptr& weapon); + +/// Can attacker operate in victim's environment? +/// e.g. If attacker is a fish, is victim in water? Or, if attacker can't swim, is victim on land? +bool isEnvironmentCompatible(const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim); + } #endif diff --git a/apps/openmw/mwmechanics/creaturestats.cpp b/apps/openmw/mwmechanics/creaturestats.cpp index 71217dbb9..05ea9fb5e 100644 --- a/apps/openmw/mwmechanics/creaturestats.cpp +++ b/apps/openmw/mwmechanics/creaturestats.cpp @@ -17,10 +17,10 @@ namespace MWMechanics CreatureStats::CreatureStats() : mLevel (0), mDead (false), mDied (false), mMurdered(false), mFriendlyHits (0), mTalkedTo (false), mAlarmed (false), - mAttacked (false), mHostile (false), + mAttacked (false), mAttackingOrSpell(false), mIsWerewolf(false), - mFallHeight(0), mRecalcDynamicStats(false), mKnockdown(false), mKnockdownOneFrame(false), + mFallHeight(0), mRecalcMagicka(false), mKnockdown(false), mKnockdownOneFrame(false), mKnockdownOverOneFrame(false), mHitRecovery(false), mBlock(false), mMovementFlags(0), mDrawState (DrawState_Nothing), mAttackStrength(0.f), mLastRestock(0,0), mGoldPool(0), mActorId(-1), @@ -120,11 +120,6 @@ namespace MWMechanics return mSpells; } - void CreatureStats::setSpells(const Spells &spells) - { - mSpells = spells; - } - ActiveSpells &CreatureStats::getActiveSpells() { return mActiveSpells; @@ -152,16 +147,28 @@ namespace MWMechanics if (value != currentValue) { - if (index != ESM::Attribute::Luck - && index != ESM::Attribute::Personality - && index != ESM::Attribute::Speed) - mRecalcDynamicStats = true; + if(!mIsWerewolf) + mAttributes[index] = value; + else + mWerewolfAttributes[index] = value; + + if (index == ESM::Attribute::Intelligence) + mRecalcMagicka = true; + else if (index == ESM::Attribute::Strength || + index == ESM::Attribute::Willpower || + index == ESM::Attribute::Agility || + index == ESM::Attribute::Endurance) + { + int strength = getAttribute(ESM::Attribute::Strength).getModified(); + int willpower = getAttribute(ESM::Attribute::Willpower).getModified(); + int agility = getAttribute(ESM::Attribute::Agility).getModified(); + int endurance = getAttribute(ESM::Attribute::Endurance).getModified(); + DynamicStat fatigue = getFatigue(); + float diff = (strength+willpower+agility+endurance) - fatigue.getBase(); + fatigue.modify(diff); + setFatigue(fatigue); + } } - - if(!mIsWerewolf) - mAttributes[index] = value; - else - mWerewolfAttributes[index] = value; } void CreatureStats::setHealth(const DynamicStat &value) @@ -188,16 +195,13 @@ namespace MWMechanics if (index==0 && mDynamic[index].getCurrent()<1) { - if (!mDead) - mDied = true; - mDead = true; - if (mDied) - // Must increase death count immediately. There are scripts that use getDeadCount as reaction to onDeath - // and rely on the increased value. - // Would be more appropriate to use a killActor(actor) function, but we don't have access to the Ptr in CreatureStats. - MWBase::Environment::get().getMechanicsManager()->killDeadActors(); + mDynamic[index].setModifier(0); + mDynamic[index].setCurrent(0); + + if (MWBase::Environment::get().getWorld()->getGodModeState()) + MWBase::Environment::get().getMechanicsManager()->keepPlayerAlive(); } } @@ -206,18 +210,13 @@ namespace MWMechanics mLevel = level; } - void CreatureStats::setActiveSpells(const ActiveSpells &active) + void CreatureStats::modifyMagicEffects(const MagicEffects &effects) { - mActiveSpells = active; - } + if (effects.get(ESM::MagicEffect::FortifyMaximumMagicka).getModifier() + != mMagicEffects.get(ESM::MagicEffect::FortifyMaximumMagicka).getModifier()) + mRecalcMagicka = true; - void CreatureStats::setMagicEffects(const MagicEffects &effects) - { - if (effects.get(ESM::MagicEffect::FortifyMaximumMagicka).mMagnitude - != mMagicEffects.get(ESM::MagicEffect::FortifyMaximumMagicka).mMagnitude) - mRecalcDynamicStats = true; - - mMagicEffects = effects; + mMagicEffects.setModifiers(effects); } void CreatureStats::setAttackingOrSpell(bool attackingOrSpell) @@ -242,6 +241,11 @@ namespace MWMechanics return mDead; } + void CreatureStats::notifyDied() + { + mDied = true; + } + bool CreatureStats::hasDied() const { return mDied; @@ -271,11 +275,7 @@ namespace MWMechanics { if (mDead) { - if (mDynamic[0].getCurrent()<1) - { - mDynamic[0].setModified(mDynamic[0].getModified(), 1); - mDynamic[0].setCurrent(1); - } + mDynamic[0].setCurrent(mDynamic[0].getModified()); if (mDynamic[0].getCurrent()>=1) mDead = false; } @@ -331,32 +331,12 @@ namespace MWMechanics mAttacked = attacked; } - bool CreatureStats::isHostile() const - { - return mHostile; - } - - void CreatureStats::setHostile (bool hostile) - { - mHostile = hostile; - } - - bool CreatureStats::getCreatureTargetted() const - { - MWWorld::Ptr targetPtr; - if (mAiSequence.getCombatTarget(targetPtr)) - { - return targetPtr.getTypeName() == typeid(ESM::Creature).name(); - } - return false; - } - float CreatureStats::getEvasion() const { float evasion = (getAttribute(ESM::Attribute::Agility).getModified() / 5.0f) + (getAttribute(ESM::Attribute::Luck).getModified() / 10.0f); evasion *= getFatigueTerm(); - evasion += mMagicEffects.get(ESM::MagicEffect::Sanctuary).mMagnitude; + evasion += mMagicEffects.get(ESM::MagicEffect::Sanctuary).getMagnitude(); return evasion; } @@ -371,6 +351,16 @@ namespace MWMechanics return mLastHitObject; } + void CreatureStats::setLastHitAttemptObject(const std::string& objectid) + { + mLastHitAttemptObject = objectid; + } + + const std::string &CreatureStats::getLastHitAttemptObject() const + { + return mLastHitAttemptObject; + } + void CreatureStats::addToFallHeight(float height) { mFallHeight += height; @@ -385,14 +375,19 @@ namespace MWMechanics bool CreatureStats::needToRecalcDynamicStats() { - if (mRecalcDynamicStats) + if (mRecalcMagicka) { - mRecalcDynamicStats = false; + mRecalcMagicka = false; return true; } return false; } + void CreatureStats::setNeedRecalcDynamicStats(bool val) + { + mRecalcMagicka = val; + } + void CreatureStats::setKnockedDown(bool value) { mKnockdown = value; @@ -502,11 +497,13 @@ namespace MWMechanics state.mDead = mDead; state.mDied = mDied; state.mMurdered = mMurdered; - state.mFriendlyHits = mFriendlyHits; + // The vanilla engine does not store friendly hits in the save file. Since there's no other mechanism + // that ever resets the friendly hits (at least not to my knowledge) this should be regarded a feature + // rather than a bug. + //state.mFriendlyHits = mFriendlyHits; state.mTalkedTo = mTalkedTo; state.mAlarmed = mAlarmed; state.mAttacked = mAttacked; - state.mHostile = mHostile; state.mAttackingOrSpell = mAttackingOrSpell; // TODO: rewrite. does this really need 3 separate bools? state.mKnockdown = mKnockdown; @@ -518,7 +515,8 @@ namespace MWMechanics state.mAttackStrength = mAttackStrength; state.mFallHeight = mFallHeight; // TODO: vertical velocity (move from PhysicActor to CreatureStats?) state.mLastHitObject = mLastHitObject; - state.mRecalcDynamicStats = mRecalcDynamicStats; + state.mLastHitAttemptObject = mLastHitAttemptObject; + state.mRecalcDynamicStats = mRecalcMagicka; state.mDrawState = mDrawState; state.mLevel = mLevel; state.mActorId = mActorId; @@ -527,6 +525,7 @@ namespace MWMechanics mSpells.writeState(state.mSpells); mActiveSpells.writeState(state.mActiveSpells); mAiSequence.writeState(state.mAiSequence); + mMagicEffects.writeState(state.mMagicEffects); state.mSummonedCreatureMap = mSummonedCreatures; state.mSummonGraveyard = mSummonGraveyard; @@ -546,16 +545,13 @@ namespace MWMechanics mLastRestock = MWWorld::TimeStamp(state.mTradeTime); mGoldPool = state.mGoldPool; - mFallHeight = state.mFallHeight; mDead = state.mDead; mDied = state.mDied; mMurdered = state.mMurdered; - mFriendlyHits = state.mFriendlyHits; mTalkedTo = state.mTalkedTo; mAlarmed = state.mAlarmed; mAttacked = state.mAttacked; - mHostile = state.mHostile; mAttackingOrSpell = state.mAttackingOrSpell; // TODO: rewrite. does this really need 3 separate bools? mKnockdown = state.mKnockdown; @@ -567,7 +563,8 @@ namespace MWMechanics mAttackStrength = state.mAttackStrength; mFallHeight = state.mFallHeight; mLastHitObject = state.mLastHitObject; - mRecalcDynamicStats = state.mRecalcDynamicStats; + mLastHitAttemptObject = state.mLastHitAttemptObject; + mRecalcMagicka = state.mRecalcDynamicStats; mDrawState = DrawState_(state.mDrawState); mLevel = state.mLevel; mActorId = state.mActorId; @@ -576,6 +573,7 @@ namespace MWMechanics mSpells.readState(state.mSpells); mActiveSpells.readState(state.mActiveSpells); mAiSequence.readState(state.mAiSequence); + mMagicEffects.readState(state.mMagicEffects); mSummonedCreatures = state.mSummonedCreatureMap; mSummonGraveyard = state.mSummonGraveyard; @@ -644,7 +642,7 @@ namespace MWMechanics mDeathAnimation = index; } - std::map& CreatureStats::getSummonedCreatureMap() + std::map& CreatureStats::getSummonedCreatureMap() { return mSummonedCreatures; } diff --git a/apps/openmw/mwmechanics/creaturestats.hpp b/apps/openmw/mwmechanics/creaturestats.hpp index a83da4249..145eb8a5b 100644 --- a/apps/openmw/mwmechanics/creaturestats.hpp +++ b/apps/openmw/mwmechanics/creaturestats.hpp @@ -40,7 +40,6 @@ namespace MWMechanics bool mTalkedTo; bool mAlarmed; bool mAttacked; - bool mHostile; bool mAttackingOrSpell; bool mKnockdown; bool mKnockdownOneFrame; @@ -53,9 +52,9 @@ namespace MWMechanics float mFallHeight; std::string mLastHitObject; // The last object to hit this actor + std::string mLastHitAttemptObject; // The last object to attempt to hit this actor - // Do we need to recalculate stats derived from attributes or other factors? - bool mRecalcDynamicStats; + bool mRecalcMagicka; // For merchants: the last time items were restocked and gold pool refilled. MWWorld::TimeStamp mLastRestock; @@ -68,8 +67,11 @@ namespace MWMechanics // The index of the death animation that was played unsigned char mDeathAnimation; - // - std::map mSummonedCreatures; + public: + typedef std::pair SummonKey; // + private: + std::map mSummonedCreatures; // + // Contains ActorIds of summoned creatures with an expired lifetime that have not been deleted yet. // This may be necessary when the creature is in an inactive cell. std::vector mSummonGraveyard; @@ -92,6 +94,7 @@ namespace MWMechanics void setAttackStrength(float value); bool needToRecalcDynamicStats(); + void setNeedRecalcDynamicStats(bool val); void addToFallHeight(float height); @@ -137,11 +140,8 @@ namespace MWMechanics void setDynamic (int index, const DynamicStat &value); - void setSpells(const Spells &spells); - - void setActiveSpells(const ActiveSpells &active); - - void setMagicEffects(const MagicEffects &effects); + /// Set Modifier for each magic effect according to \a effects. Does not touch Base values. + void modifyMagicEffects(const MagicEffects &effects); void setAttackingOrSpell(bool attackingOrSpell); @@ -167,6 +167,8 @@ namespace MWMechanics bool isDead() const; + void notifyDied(); + bool hasDied() const; void clearHasDied(); @@ -200,11 +202,6 @@ namespace MWMechanics bool getAttacked() const; void setAttacked (bool attacked); - bool isHostile() const; - void setHostile (bool hostile); - - bool getCreatureTargetted() const; - float getEvasion() const; void setKnockedDown(bool value); @@ -222,15 +219,17 @@ namespace MWMechanics void setBlock(bool value); bool getBlock() const; - std::map& getSummonedCreatureMap(); - std::vector& getSummonedCreatureGraveyard(); + std::map& getSummonedCreatureMap(); // + std::vector& getSummonedCreatureGraveyard(); // ActorIds enum Flag { Flag_ForceRun = 1, Flag_ForceSneak = 2, Flag_Run = 4, - Flag_Sneak = 8 + Flag_Sneak = 8, + Flag_ForceJump = 16, + Flag_ForceMoveJump = 32 }; enum Stance { @@ -244,7 +243,9 @@ namespace MWMechanics bool getStance (Stance flag) const; void setLastHitObject(const std::string &objectid); + void setLastHitAttemptObject(const std::string &objectid); const std::string &getLastHitObject() const; + const std::string &getLastHitAttemptObject() const; // Note, this is just a cache to avoid checking the whole container store every frame. We don't need to store it in saves. // TODO: Put it somewhere else? diff --git a/apps/openmw/mwmechanics/disease.hpp b/apps/openmw/mwmechanics/disease.hpp index 05ce1c7ae..a973c0e35 100644 --- a/apps/openmw/mwmechanics/disease.hpp +++ b/apps/openmw/mwmechanics/disease.hpp @@ -25,6 +25,8 @@ namespace MWMechanics MWBase::Environment::get().getWorld()->getStore().get().find( "fDiseaseXferChance")->getFloat(); + MagicEffects& actorEffects = actor.getClass().getCreatureStats(actor).getMagicEffects(); + Spells& spells = carrier.getClass().getCreatureStats(carrier).getSpells(); for (Spells::TIterator it = spells.begin(); it != spells.end(); ++it) { @@ -33,26 +35,16 @@ namespace MWMechanics if (actor.getClass().getCreatureStats(actor).getSpells().hasSpell(spell->mId)) continue; - bool hasCorprusEffect = false; - for (std::vector::const_iterator effectIt = spell->mEffects.mList.begin(); effectIt != spell->mEffects.mList.end(); ++effectIt) - { - if (effectIt->mEffectID == ESM::MagicEffect::Corprus) - { - hasCorprusEffect = true; - break; - } - } - float resist = 0.f; - if (hasCorprusEffect) - resist = 1.f - 0.01 * (actor.getClass().getCreatureStats(actor).getMagicEffects().get(ESM::MagicEffect::ResistCorprusDisease).mMagnitude - - actor.getClass().getCreatureStats(actor).getMagicEffects().get(ESM::MagicEffect::WeaknessToCorprusDisease).mMagnitude); + if (spells.hasCorprusEffect(spell)) + resist = 1.f - 0.01 * (actorEffects.get(ESM::MagicEffect::ResistCorprusDisease).getMagnitude() + - actorEffects.get(ESM::MagicEffect::WeaknessToCorprusDisease).getMagnitude()); else if (spell->mData.mType == ESM::Spell::ST_Disease) - resist = 1.f - 0.01 * (actor.getClass().getCreatureStats(actor).getMagicEffects().get(ESM::MagicEffect::ResistCommonDisease).mMagnitude - - actor.getClass().getCreatureStats(actor).getMagicEffects().get(ESM::MagicEffect::WeaknessToCommonDisease).mMagnitude); + resist = 1.f - 0.01 * (actorEffects.get(ESM::MagicEffect::ResistCommonDisease).getMagnitude() + - actorEffects.get(ESM::MagicEffect::WeaknessToCommonDisease).getMagnitude()); else if (spell->mData.mType == ESM::Spell::ST_Blight) - resist = 1.f - 0.01 * (actor.getClass().getCreatureStats(actor).getMagicEffects().get(ESM::MagicEffect::ResistBlightDisease).mMagnitude - - actor.getClass().getCreatureStats(actor).getMagicEffects().get(ESM::MagicEffect::WeaknessToBlightDisease).mMagnitude); + resist = 1.f - 0.01 * (actorEffects.get(ESM::MagicEffect::ResistBlightDisease).getMagnitude() + - actorEffects.get(ESM::MagicEffect::WeaknessToBlightDisease).getMagnitude()); else continue; diff --git a/apps/openmw/mwmechanics/enchanting.cpp b/apps/openmw/mwmechanics/enchanting.cpp index f3f6795db..de5921a70 100644 --- a/apps/openmw/mwmechanics/enchanting.cpp +++ b/apps/openmw/mwmechanics/enchanting.cpp @@ -2,10 +2,12 @@ #include "../mwworld/manualref.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "creaturestats.hpp" #include "npcstats.hpp" +#include "spellcasting.hpp" namespace MWMechanics { @@ -53,6 +55,9 @@ namespace MWMechanics MWWorld::ContainerStore& store = player.getClass().getContainerStore(player); ESM::Enchantment enchantment; enchantment.mData.mCharge = getGemCharge(); + enchantment.mData.mAutocalc = 0; + enchantment.mData.mType = mCastStyle; + enchantment.mData.mCost = getBaseCastCost(); store.remove(mSoulGemPtr, 1, player); @@ -72,8 +77,6 @@ namespace MWMechanics { enchantment.mData.mCharge=0; } - enchantment.mData.mType = mCastStyle; - enchantment.mData.mCost = getEnchantPoints(); enchantment.mEffects = mEffectList; // Apply the enchantment @@ -155,7 +158,7 @@ namespace MWMechanics * * Formula on UESPWiki is not entirely correct. */ - float Enchanting::getEnchantPoints() const + int Enchanting::getEnchantPoints() const { if (mEffectList.mList.empty()) // No effects added, cost = 0 @@ -166,56 +169,53 @@ namespace MWMechanics float enchantmentCost = 0; int effectsLeftCnt = mEffects.size(); - float baseCost, magnitudeCost, areaCost; - int magMin, magMax, area; for (std::vector::const_iterator it = mEffects.begin(); it != mEffects.end(); ++it) { - baseCost = (store.get().find(it->mEffectID))->mData.mBaseCost; - // To reflect vanilla behavior - magMin = (it->mMagnMin == 0) ? 1 : it->mMagnMin; - magMax = (it->mMagnMax == 0) ? 1 : it->mMagnMax; - area = (it->mArea == 0) ? 1 : it->mArea; + float baseCost = (store.get().find(it->mEffectID))->mData.mBaseCost; + int magMin = (it->mMagnMin == 0) ? 1 : it->mMagnMin; + int magMax = (it->mMagnMax == 0) ? 1 : it->mMagnMax; + int area = (it->mArea == 0) ? 1 : it->mArea; + float magnitudeCost = (magMin + magMax) * baseCost * 0.05; if (mCastStyle == ESM::Enchantment::ConstantEffect) { - magnitudeCost = (magMin + magMax) * baseCost * 2.5; + magnitudeCost *= store.get().find("fEnchantmentConstantDurationMult")->getFloat(); } else { - magnitudeCost = (magMin + magMax) * it->mDuration * baseCost * 0.025; - if(it->mRange == ESM::RT_Target) - magnitudeCost *= 1.5; + magnitudeCost *= it->mDuration; } - areaCost = area * 0.025 * baseCost; + float areaCost = area * 0.05 * baseCost; + + const float fEffectCostMult = store.get().find("fEffectCostMult")->getFloat(); + + float cost = (magnitudeCost + areaCost) * fEffectCostMult; if (it->mRange == ESM::RT_Target) - areaCost *= 1.5; + cost *= 1.5; - enchantmentCost += (magnitudeCost + areaCost) * effectsLeftCnt; + enchantmentCost += cost * effectsLeftCnt; + enchantmentCost = std::max(1.f, enchantmentCost); --effectsLeftCnt; } - return enchantmentCost; + return static_cast(enchantmentCost); } - float Enchanting::getCastCost() const + int Enchanting::getBaseCastCost() const { if (mCastStyle == ESM::Enchantment::ConstantEffect) return 0; - const float enchantCost = getEnchantPoints(); - MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); - MWMechanics::NpcStats &stats = player.getClass().getNpcStats(player); - int eSkill = stats.getSkill(ESM::Skill::Enchant).getModified(); - - /* - * Each point of enchant skill above/under 10 subtracts/adds - * one percent of enchantment cost while minimum is 1. - */ - const float castCost = enchantCost - (enchantCost / 100) * (eSkill - 10); + return getEnchantPoints(); + } - return (castCost < 1) ? 1 : castCost; + int Enchanting::getEffectiveCastCost() const + { + int baseCost = getBaseCastCost(); + MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); + return getEffectiveEnchantmentCastCost(baseCost, player); } @@ -240,7 +240,7 @@ namespace MWMechanics return soul->mData.mSoul; } - float Enchanting::getMaxEnchantValue() const + int Enchanting::getMaxEnchantValue() const { if (itemEmpty()) return 0; @@ -292,5 +292,9 @@ namespace MWMechanics MWWorld::ContainerStore& store = player.getClass().getContainerStore(player); store.remove(MWWorld::ContainerStore::sGoldId, getEnchantPrice(), player); + + // add gold to NPC trading gold pool + CreatureStats& enchanterStats = mEnchanter.getClass().getCreatureStats(mEnchanter); + enchanterStats.setGoldPool(enchanterStats.getGoldPool() + getEnchantPrice()); } } diff --git a/apps/openmw/mwmechanics/enchanting.hpp b/apps/openmw/mwmechanics/enchanting.hpp index 01ca1e0e1..2c05d2d2e 100644 --- a/apps/openmw/mwmechanics/enchanting.hpp +++ b/apps/openmw/mwmechanics/enchanting.hpp @@ -35,10 +35,11 @@ namespace MWMechanics bool create(); //Return true if created, false if failed. void nextCastStyle(); //Set enchant type to next possible type (for mOldItemPtr object) int getCastStyle() const; - float getEnchantPoints() const; - float getCastCost() const; + int getEnchantPoints() const; + int getBaseCastCost() const; // To be saved in the enchantment's record + int getEffectiveCastCost() const; // Effective cost taking player Enchant skill into account, used for preview purposes in the UI int getEnchantPrice() const; - float getMaxEnchantValue() const; + int getMaxEnchantValue() const; int getGemCharge() const; float getEnchantChance() const; bool soulEmpty() const; //Return true if empty diff --git a/apps/openmw/mwmechanics/levelledlist.hpp b/apps/openmw/mwmechanics/levelledlist.hpp index 5d9e29118..691996410 100644 --- a/apps/openmw/mwmechanics/levelledlist.hpp +++ b/apps/openmw/mwmechanics/levelledlist.hpp @@ -2,6 +2,7 @@ #define OPENMW_MECHANICS_LEVELLEDLIST_H #include "../mwworld/ptr.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwworld/manualref.hpp" #include "../mwworld/class.hpp" #include "../mwbase/world.hpp" @@ -12,9 +13,9 @@ namespace MWMechanics { /// @return ID of resulting item, or empty if none - inline std::string getLevelledItem (const ESM::LeveledListBase* levItem, bool creature, unsigned char failChance=0) + inline std::string getLevelledItem (const ESM::LevelledListBase* levItem, bool creature, unsigned char failChance=0) { - const std::vector& items = levItem->mList; + const std::vector& items = levItem->mList; const MWWorld::Ptr& player = MWBase::Environment::get().getWorld()->getPlayerPtr(); int playerLevel = player.getClass().getCreatureStats(player).getLevel(); @@ -27,7 +28,7 @@ namespace MWMechanics std::vector candidates; int highestLevel = 0; - for (std::vector::const_iterator it = items.begin(); it != items.end(); ++it) + for (std::vector::const_iterator it = items.begin(); it != items.end(); ++it) { if (it->mLevel > highestLevel && it->mLevel <= playerLevel) highestLevel = it->mLevel; @@ -39,7 +40,7 @@ namespace MWMechanics allLevels = levItem->mFlags & ESM::CreatureLevList::AllLevels; std::pair highest = std::make_pair(-1, ""); - for (std::vector::const_iterator it = items.begin(); it != items.end(); ++it) + for (std::vector::const_iterator it = items.begin(); it != items.end(); ++it) { if (playerLevel >= it->mLevel && (allLevels || it->mLevel == highestLevel)) @@ -70,9 +71,9 @@ namespace MWMechanics else { if (ref.getPtr().getTypeName() == typeid(ESM::ItemLevList).name()) - return getLevelledItem(ref.getPtr().get()->mBase, failChance); + return getLevelledItem(ref.getPtr().get()->mBase, false, failChance); else - return getLevelledItem(ref.getPtr().get()->mBase, failChance); + return getLevelledItem(ref.getPtr().get()->mBase, true, failChance); } } diff --git a/apps/openmw/mwmechanics/magiceffects.cpp b/apps/openmw/mwmechanics/magiceffects.cpp index 5be0854ab..0b19df0a8 100644 --- a/apps/openmw/mwmechanics/magiceffects.cpp +++ b/apps/openmw/mwmechanics/magiceffects.cpp @@ -6,6 +6,7 @@ #include #include +#include namespace MWMechanics { @@ -40,20 +41,57 @@ namespace MWMechanics return left.mArgsecond.setModifier(effects.get(it->first).getModifier()); + } + + for (Collection::const_iterator it = effects.begin(); it != effects.end(); ++it) + { + mCollection[it->first].setModifier(it->second.getModifier()); + } + } + MagicEffects& MagicEffects::operator+= (const MagicEffects& effects) { if (this==&effects) @@ -137,4 +193,25 @@ namespace MWMechanics return result; } + + void MagicEffects::writeState(ESM::MagicEffects &state) const + { + // Don't need to save Modifiers, they are recalculated every frame anyway. + for (Collection::const_iterator iter (begin()); iter!=end(); ++iter) + { + if (iter->second.getBase() != 0) + { + // Don't worry about mArg, never used by magic effect script instructions + state.mEffects.insert(std::make_pair(iter->first.mId, iter->second.getBase())); + } + } + } + + void MagicEffects::readState(const ESM::MagicEffects &state) + { + for (std::map::const_iterator it = state.mEffects.begin(); it != state.mEffects.end(); ++it) + { + mCollection[EffectKey(it->first)].setBase(it->second); + } + } } diff --git a/apps/openmw/mwmechanics/magiceffects.hpp b/apps/openmw/mwmechanics/magiceffects.hpp index 4fd5e159a..86f5a1804 100644 --- a/apps/openmw/mwmechanics/magiceffects.hpp +++ b/apps/openmw/mwmechanics/magiceffects.hpp @@ -8,6 +8,8 @@ namespace ESM { struct ENAMstruct; struct EffectList; + + struct MagicEffects; } namespace MWMechanics @@ -28,12 +30,27 @@ namespace MWMechanics struct EffectParam { - // Note usually this would be int, but applying partial resistance might introduce decimal point. - float mMagnitude; + private: + // Note usually this would be int, but applying partial resistance might introduce a decimal point. + float mModifier; + + int mBase; + + public: + /// Get the total magnitude including base and modifier. + float getMagnitude() const; + + void setModifier(float mod); + float getModifier() const; + + /// Change mBase by \a diff + void modifyBase(int diff); + void setBase(int base); + int getBase() const; EffectParam(); - EffectParam(float magnitude) : mMagnitude(magnitude) {} + EffectParam(float magnitude) : mModifier(magnitude), mBase(0) {} EffectParam& operator+= (const EffectParam& param); @@ -55,9 +72,11 @@ namespace MWMechanics // Used by effect management classes (ActiveSpells, InventoryStore, Spells) to list active effect sources for GUI display struct EffectSourceVisitor { + virtual ~EffectSourceVisitor() { } + virtual void visit (MWMechanics::EffectKey key, - const std::string& sourceName, int casterActorId, - float magnitude, float remainingTime = -1) = 0; + const std::string& sourceName, const std::string& sourceId, int casterActorId, + float magnitude, float remainingTime = -1, float totalTime = -1) = 0; }; /// \brief Effects currently affecting a NPC or creature @@ -77,7 +96,16 @@ namespace MWMechanics Collection::const_iterator end() const { return mCollection.end(); } + void readState (const ESM::MagicEffects& state); + void writeState (ESM::MagicEffects& state) const; + void add (const EffectKey& key, const EffectParam& param); + void remove (const EffectKey& key); + + void modifyBase (const EffectKey& key, int diff); + + /// Copy Modifier values from \a effects, but keep original mBase values. + void setModifiers(const MagicEffects& effects); MagicEffects& operator+= (const MagicEffects& effects); diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index 861d5e110..4e4a1a8a6 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -2,6 +2,8 @@ #include "mechanicsmanagerimp.hpp" #include "npcstats.hpp" +#include + #include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" @@ -21,37 +23,33 @@ #include "spellcasting.hpp" #include "autocalcspell.hpp" +#include + namespace { - /// @return is \a ptr allowed to take/use \a item or is it a crime? - bool isAllowedToUse (const MWWorld::Ptr& ptr, const MWWorld::Ptr& item, MWWorld::Ptr& victim) + + float getFightDistanceBias(const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2) { - const std::string& owner = item.getCellRef().getOwner(); - bool isOwned = !owner.empty() && owner != "player"; + Ogre::Vector3 pos1 (actor1.getRefData().getPosition().pos); + Ogre::Vector3 pos2 (actor2.getRefData().getPosition().pos); - const std::string& faction = item.getCellRef().getFaction(); - bool isFactionOwned = false; - if (!faction.empty() && ptr.getClass().isNpc()) - { - const std::map& factions = ptr.getClass().getNpcStats(ptr).getFactionRanks(); - std::map::const_iterator found = factions.find(Misc::StringUtils::lowerCase(faction)); - if (found == factions.end() - || found->second < item.getCellRef().getFactionRank()) - isFactionOwned = true; - } + float d = pos1.distance(pos2); - const std::string& globalVariable = item.getCellRef().getGlobalVariable(); - if (!globalVariable.empty() && MWBase::Environment::get().getWorld()->getGlobalInt(Misc::StringUtils::lowerCase(globalVariable)) == 1) - { - isOwned = false; - isFactionOwned = false; - } + static const int iFightDistanceBase = MWBase::Environment::get().getWorld()->getStore().get().find( + "iFightDistanceBase")->getInt(); + static const float fFightDistanceMultiplier = MWBase::Environment::get().getWorld()->getStore().get().find( + "fFightDistanceMultiplier")->getFloat(); - if (!item.getCellRef().getOwner().empty()) - victim = MWBase::Environment::get().getWorld()->searchPtr(item.getCellRef().getOwner(), true); + return (iFightDistanceBase - fFightDistanceMultiplier * d); + } - return (!isOwned && !isFactionOwned); + float getFightDispositionBias(float disposition) + { + static const float fFightDispMult = MWBase::Environment::get().getWorld()->getStore().get().find( + "fFightDispMult")->getFloat(); + return ((50.f - disposition) * fFightDispMult); } + } namespace MWMechanics @@ -68,7 +66,7 @@ namespace MWMechanics // reset creatureStats.setLevel(player->mNpdt52.mLevel); creatureStats.getSpells().clear(); - creatureStats.setMagicEffects(MagicEffects()); + creatureStats.modifyMagicEffects(MagicEffects()); for (int i=0; i<27; ++i) npcStats.getSkill (i).setBase (player->mNpdt52.mSkills[i]); @@ -174,9 +172,9 @@ namespace MWMechanics MWWorld::Store::iterator iter = skills.begin(); for (; iter != skills.end(); ++iter) { - if (iter->mData.mSpecialization==class_->mData.mSpecialization) + if (iter->second.mData.mSpecialization==class_->mData.mSpecialization) { - int index = iter->mIndex; + int index = iter->first; if (index>=0 && index<27) { @@ -286,7 +284,7 @@ namespace MWMechanics } MechanicsManager::MechanicsManager() - : mUpdatePlayer (true), mClassSelected (false), + : mWatchedStatsEmpty (true), mUpdatePlayer (true), mClassSelected (false), mRaceSelected (false), mAI(true) { //buildPlayer no longer here, needs to be done explicitely after all subsystems are up and running @@ -348,7 +346,7 @@ namespace MWMechanics const MWMechanics::NpcStats &stats = mWatched.getClass().getNpcStats(mWatched); for(int i = 0;i < ESM::Attribute::Length;++i) { - if(stats.getAttribute(i) != mWatchedStats.getAttribute(i)) + if(stats.getAttribute(i) != mWatchedStats.getAttribute(i) || mWatchedStatsEmpty) { std::stringstream attrname; attrname << "AttribVal"<<(i+1); @@ -358,19 +356,19 @@ namespace MWMechanics } } - if(stats.getHealth() != mWatchedStats.getHealth()) + if(stats.getHealth() != mWatchedStats.getHealth() || mWatchedStatsEmpty) { static const std::string hbar("HBar"); mWatchedStats.setHealth(stats.getHealth()); winMgr->setValue(hbar, stats.getHealth()); } - if(stats.getMagicka() != mWatchedStats.getMagicka()) + if(stats.getMagicka() != mWatchedStats.getMagicka() || mWatchedStatsEmpty) { static const std::string mbar("MBar"); mWatchedStats.setMagicka(stats.getMagicka()); winMgr->setValue(mbar, stats.getMagicka()); } - if(stats.getFatigue() != mWatchedStats.getFatigue()) + if(stats.getFatigue() != mWatchedStats.getFatigue() || mWatchedStatsEmpty) { static const std::string fbar("FBar"); mWatchedStats.setFatigue(stats.getFatigue()); @@ -396,7 +394,7 @@ namespace MWMechanics //Loop over ESM::Skill::SkillEnum for(int i = 0; i < ESM::Skill::Length; ++i) { - if(stats.getSkill(i) != mWatchedStats.getSkill(i)) + if(stats.getSkill(i) != mWatchedStats.getSkill(i) || mWatchedStatsEmpty) { update = true; mWatchedStats.getSkill(i) = stats.getSkill(i); @@ -409,6 +407,8 @@ namespace MWMechanics winMgr->setValue("level", stats.getLevel()); + mWatchedStatsEmpty = false; + // Update the equipped weapon icon MWWorld::InventoryStore& inv = mWatched.getClass().getInventoryStore(mWatched); MWWorld::ContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); @@ -474,6 +474,7 @@ namespace MWMechanics void MechanicsManager::rest(bool sleep) { mActors.restoreDynamicStats (sleep); + mActors.fastForwardAi(); } int MechanicsManager::getHoursToRest() const @@ -562,16 +563,18 @@ namespace MWMechanics MWWorld::LiveCellRef* player = playerPtr.get(); const MWMechanics::NpcStats &playerStats = playerPtr.getClass().getNpcStats(playerPtr); + const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); + static const float fDispRaceMod = gmst.find("fDispRaceMod")->getFloat(); if (Misc::StringUtils::ciEqual(npc->mBase->mRace, player->mBase->mRace)) - x += MWBase::Environment::get().getWorld()->getStore().get().find("fDispRaceMod")->getFloat(); + x += fDispRaceMod; - x += MWBase::Environment::get().getWorld()->getStore().get().find("fDispPersonalityMult")->getFloat() - * (playerStats.getAttribute(ESM::Attribute::Personality).getModified() - MWBase::Environment::get().getWorld()->getStore().get().find("fDispPersonalityBase")->getFloat()); + static const float fDispPersonalityMult = gmst.find("fDispPersonalityMult")->getFloat(); + static const float fDispPersonalityBase = gmst.find("fDispPersonalityBase")->getFloat(); + x += fDispPersonalityMult * (playerStats.getAttribute(ESM::Attribute::Personality).getModified() - fDispPersonalityBase); float reaction = 0; int rank = 0; - std::string npcFaction = ""; - if(!npcSkill.getFactionRanks().empty()) npcFaction = npcSkill.getFactionRanks().begin()->first; + std::string npcFaction = ptr.getClass().getPrimaryFaction(ptr); Misc::StringUtils::toLower(npcFaction); @@ -602,18 +605,25 @@ namespace MWMechanics reaction = 0; rank = 0; } - x += (MWBase::Environment::get().getWorld()->getStore().get().find("fDispFactionRankMult")->getFloat() * rank - + MWBase::Environment::get().getWorld()->getStore().get().find("fDispFactionRankBase")->getFloat()) - * MWBase::Environment::get().getWorld()->getStore().get().find("fDispFactionMod")->getFloat() * reaction; - x -= MWBase::Environment::get().getWorld()->getStore().get().find("fDispCrimeMod")->getFloat() * playerStats.getBounty(); + static const float fDispFactionRankMult = gmst.find("fDispFactionRankMult")->getFloat(); + static const float fDispFactionRankBase = gmst.find("fDispFactionRankBase")->getFloat(); + static const float fDispFactionMod = gmst.find("fDispFactionMod")->getFloat(); + x += (fDispFactionRankMult * rank + + fDispFactionRankBase) + * fDispFactionMod * reaction; + + static const float fDispCrimeMod = gmst.find("fDispCrimeMod")->getFloat(); + static const float fDispDiseaseMod = gmst.find("fDispDiseaseMod")->getFloat(); + x -= fDispCrimeMod * playerStats.getBounty(); if (playerStats.hasCommonDisease() || playerStats.hasBlightDisease()) - x += MWBase::Environment::get().getWorld()->getStore().get().find("fDispDiseaseMod")->getFloat(); + x += fDispDiseaseMod; + static const float fDispWeaponDrawn = gmst.find("fDispWeaponDrawn")->getFloat(); if (playerStats.getDrawState() == MWMechanics::DrawState_Weapon) - x += MWBase::Environment::get().getWorld()->getStore().get().find("fDispWeaponDrawn")->getFloat(); + x += fDispWeaponDrawn; - x += ptr.getClass().getCreatureStats(ptr).getMagicEffects().get(ESM::MagicEffect::Charm).mMagnitude; + x += ptr.getClass().getCreatureStats(ptr).getMagicEffects().get(ESM::MagicEffect::Charm).getMagnitude(); int effective_disposition = std::max(0,std::min(int(x),100));//, normally clamped to [0..100] when used return effective_disposition; @@ -662,11 +672,6 @@ namespace MWMechanics return mActors.countDeaths (id); } - void MechanicsManager::killDeadActors() - { - mActors.killDeadActors(); - } - void MechanicsManager::getPersuasionDispositionChange (const MWWorld::Ptr& npc, PersuasionType type, float currentTemporaryDispositionDelta, bool& success, float& tempChange, float& permChange) { @@ -870,15 +875,50 @@ namespace MWMechanics mAI = true; } + bool MechanicsManager::isAllowedToUse (const MWWorld::Ptr& ptr, const MWWorld::CellRef& cellref, MWWorld::Ptr& victim) + { + const std::string& owner = cellref.getOwner(); + bool isOwned = !owner.empty() && owner != "player"; + + const std::string& faction = cellref.getFaction(); + bool isFactionOwned = false; + if (!faction.empty() && ptr.getClass().isNpc()) + { + const std::map& factions = ptr.getClass().getNpcStats(ptr).getFactionRanks(); + std::map::const_iterator found = factions.find(Misc::StringUtils::lowerCase(faction)); + if (found == factions.end() + || found->second < cellref.getFactionRank()) + isFactionOwned = true; + } + + const std::string& globalVariable = cellref.getGlobalVariable(); + if (!globalVariable.empty() && MWBase::Environment::get().getWorld()->getGlobalInt(Misc::StringUtils::lowerCase(globalVariable)) == 1) + { + isOwned = false; + isFactionOwned = false; + } + + if (!cellref.getOwner().empty()) + victim = MWBase::Environment::get().getWorld()->searchPtr(cellref.getOwner(), true); + + return (!isOwned && !isFactionOwned); + } + bool MechanicsManager::sleepInBed(const MWWorld::Ptr &ptr, const MWWorld::Ptr &bed) { + if (ptr.getClass().getNpcStats(ptr).isWerewolf()) + { + MWBase::Environment::get().getWindowManager()->messageBox("#{sWerewolfRefusal}"); + return true; + } + if(MWBase::Environment::get().getWorld()->getPlayer().isInCombat()) { MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage2}"); return true; } MWWorld::Ptr victim; - if (isAllowedToUse(ptr, bed, victim)) + if (isAllowedToUse(ptr, bed.getCellRef(), victim)) return false; if(commitCrime(ptr, victim, OT_SleepingInOwnedBed)) @@ -893,51 +933,119 @@ namespace MWMechanics void MechanicsManager::objectOpened(const MWWorld::Ptr &ptr, const MWWorld::Ptr &item) { MWWorld::Ptr victim; - if (isAllowedToUse(ptr, item, victim)) + if (isAllowedToUse(ptr, item.getCellRef(), victim)) return; commitCrime(ptr, victim, OT_Trespassing); } - void MechanicsManager::itemTaken(const MWWorld::Ptr &ptr, const MWWorld::Ptr &item, int count) + std::vector > MechanicsManager::getStolenItemOwners(const std::string& itemid) + { + std::vector > result; + StolenItemsMap::const_iterator it = mStolenItems.find(Misc::StringUtils::lowerCase(itemid)); + if (it == mStolenItems.end()) + return result; + else + { + const OwnerMap& owners = it->second; + for (OwnerMap::const_iterator ownerIt = owners.begin(); ownerIt != owners.end(); ++ownerIt) + result.push_back(std::make_pair(ownerIt->first.first, ownerIt->second)); + return result; + } + } + + bool MechanicsManager::isItemStolenFrom(const std::string &itemid, const std::string &ownerid) + { + StolenItemsMap::const_iterator it = mStolenItems.find(Misc::StringUtils::lowerCase(itemid)); + if (it == mStolenItems.end()) + return false; + const OwnerMap& owners = it->second; + OwnerMap::const_iterator ownerFound = owners.find(std::make_pair(Misc::StringUtils::lowerCase(ownerid), false)); + return ownerFound != owners.end(); + } + + void MechanicsManager::confiscateStolenItems(const MWWorld::Ptr &player, const MWWorld::Ptr &targetContainer) + { + MWWorld::ContainerStore& store = player.getClass().getContainerStore(player); + for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) + { + StolenItemsMap::iterator stolenIt = mStolenItems.find(Misc::StringUtils::lowerCase(it->getClass().getId(*it))); + if (stolenIt == mStolenItems.end()) + continue; + OwnerMap& owners = stolenIt->second; + int itemCount = it->getRefData().getCount(); + for (OwnerMap::iterator ownerIt = owners.begin(); ownerIt != owners.end();) + { + int toRemove = std::min(itemCount, ownerIt->second); + itemCount -= toRemove; + ownerIt->second -= toRemove; + if (ownerIt->second == 0) + owners.erase(ownerIt++); + else + ++ownerIt; + } + + int toMove = it->getRefData().getCount() - itemCount; + + targetContainer.getClass().getContainerStore(targetContainer).add(*it, toMove, targetContainer); + store.remove(*it, toMove, player); + } + // TODO: unhardcode the locklevel + targetContainer.getClass().lock(targetContainer,50); + } + + void MechanicsManager::itemTaken(const MWWorld::Ptr &ptr, const MWWorld::Ptr &item, const MWWorld::Ptr& container, + int count) { + if (ptr != MWBase::Environment::get().getWorld()->getPlayerPtr()) + return; + MWWorld::Ptr victim; - if (isAllowedToUse(ptr, item, victim)) + + const MWWorld::CellRef* ownerCellRef = &item.getCellRef(); + if (!container.isEmpty()) + { + // Inherit the owner of the container + ownerCellRef = &container.getCellRef(); + } + else + { + if (!item.getCellRef().hasContentFile()) + { + // this is a manually placed item, which means it was already stolen + return; + } + } + + if (isAllowedToUse(ptr, *ownerCellRef, victim)) return; + + Owner owner; + owner.first = ownerCellRef->getOwner(); + owner.second = false; + if (owner.first.empty()) + { + owner.first = ownerCellRef->getFaction(); + owner.second = true; + } + Misc::StringUtils::toLower(owner.first); + mStolenItems[Misc::StringUtils::lowerCase(item.getClass().getId(item))][owner] += count; + commitCrime(ptr, victim, OT_Theft, item.getClass().getValue(item) * count); } - bool MechanicsManager::commitCrime(const MWWorld::Ptr &player, const MWWorld::Ptr &victim, OffenseType type, int arg) + bool MechanicsManager::commitCrime(const MWWorld::Ptr &player, const MWWorld::Ptr &victim, OffenseType type, int arg, bool victimAware) { - // NOTE: int arg can be from itemTaken() so DON'T modify it, since it is - // passed to reportCrime later on in this function. - // NOTE: victim may be empty // Only player can commit crime - if (player.getRefData().getHandle() != "player") + if (player != MWBase::Environment::get().getWorld()->getPlayerPtr()) return false; - const MWWorld::ESMStore& esmStore = MWBase::Environment::get().getWorld()->getStore(); - - // What amount of alarm did this crime generate? - int alarm = 0; - if (type == OT_Trespassing || type == OT_SleepingInOwnedBed) - alarm = esmStore.get().find("iAlarmTresspass")->getInt(); - else if (type == OT_Pickpocket) - alarm = esmStore.get().find("iAlarmPickPocket")->getInt(); - else if (type == OT_Assault) - alarm = esmStore.get().find("iAlarmAttack")->getInt(); - else if (type == OT_Murder) - alarm = esmStore.get().find("iAlarmKilling")->getInt(); - else if (type == OT_Theft) - alarm = esmStore.get().find("iAlarmStealing")->getInt(); - - bool reported = false; - // Find all the actors within the alarm radius std::vector neighbors; Ogre::Vector3 from = Ogre::Vector3(player.getRefData().getPosition().pos); + const MWWorld::ESMStore& esmStore = MWBase::Environment::get().getWorld()->getStore(); float radius = esmStore.get().find("fAlarmRadius")->getFloat(); mActors.getObjectsInRange(from, radius, neighbors); @@ -946,9 +1054,8 @@ namespace MWMechanics if (!victim.isEmpty() && from.squaredDistance(Ogre::Vector3(victim.getRefData().getPosition().pos)) > radius*radius) neighbors.push_back(victim); - bool victimAware = false; - - // Find actors who directly witnessed the crime + // Did anyone see it? + bool crimeSeen = false; for (std::vector::iterator it = neighbors.begin(); it != neighbors.end(); ++it) { if (*it == player) @@ -956,37 +1063,33 @@ namespace MWMechanics if (it->getClass().getCreatureStats(*it).isDead()) continue; - // Was the crime seen? - if ((MWBase::Environment::get().getWorld()->getLOS(player, *it) && awarenessCheck(player, *it) ) + if ((*it == victim && victimAware) + || (MWBase::Environment::get().getWorld()->getLOS(player, *it) && awarenessCheck(player, *it) ) // Murder crime can be reported even if no one saw it (hearing is enough, I guess). // TODO: Add mod support for stealth executions! || (type == OT_Murder && *it != victim)) { - if (*it == victim) - victimAware = true; - - // TODO: are there other messages? - if (type == OT_Theft) + if (type == OT_Theft || type == OT_Pickpocket) MWBase::Environment::get().getDialogueManager()->say(*it, "thief"); + else if (type == OT_Trespassing) + MWBase::Environment::get().getDialogueManager()->say(*it, "intruder"); // Crime reporting only applies to NPCs if (!it->getClass().isNpc()) continue; - // Will the witness report the crime? - if (it->getClass().getCreatureStats(*it).getAiSetting(CreatureStats::AI_Alarm).getBase() >= alarm) - { - reported = true; - } + if (it->getClass().getCreatureStats(*it).getAiSequence().isInCombat(victim)) + continue; + + crimeSeen = true; } } - if (reported) + if (crimeSeen) reportCrime(player, victim, type, arg); - else if (victimAware && !victim.isEmpty() && type == OT_Assault) - startCombat(victim, player); - - return reported; + else if (type == OT_Assault && !victim.isEmpty()) + startCombat(victim, player); // TODO: combat should be started with an "unaware" flag, which makes the victim flee? + return crimeSeen; } void MechanicsManager::reportCrime(const MWWorld::Ptr &player, const MWWorld::Ptr &victim, OffenseType type, int arg) @@ -996,37 +1099,36 @@ namespace MWMechanics if (type == OT_Murder && !victim.isEmpty()) victim.getClass().getCreatureStats(victim).notifyMurder(); - // Bounty for each type of crime + // Bounty and disposition penalty for each type of crime + float disp = 0.f, dispVictim = 0.f; if (type == OT_Trespassing || type == OT_SleepingInOwnedBed) + { arg = store.find("iCrimeTresspass")->getInt(); + disp = dispVictim = store.find("iDispTresspass")->getInt(); + } else if (type == OT_Pickpocket) + { arg = store.find("iCrimePickPocket")->getInt(); + disp = dispVictim = store.find("fDispPickPocketMod")->getFloat(); + } else if (type == OT_Assault) + { arg = store.find("iCrimeAttack")->getInt(); + disp = store.find("iDispAttackMod")->getInt(); + dispVictim = store.find("fDispAttacking")->getFloat(); + } else if (type == OT_Murder) + { arg = store.find("iCrimeKilling")->getInt(); + disp = dispVictim = store.find("iDispKilling")->getInt(); + } else if (type == OT_Theft) { + disp = dispVictim = store.find("fDispStealing")->getFloat() * arg; arg *= store.find("fCrimeStealing")->getFloat(); arg = std::max(1, arg); // Minimum bounty of 1, in case items with zero value are stolen } - MWBase::Environment::get().getWindowManager()->messageBox("#{sCrimeMessage}"); - player.getClass().getNpcStats(player).setBounty(player.getClass().getNpcStats(player).getBounty() - + arg); - - // If committing a crime against a faction member, expell from the faction - if (!victim.isEmpty() && victim.getClass().isNpc()) - { - std::string factionID; - if(!victim.getClass().getNpcStats(victim).getFactionRanks().empty()) - factionID = victim.getClass().getNpcStats(victim).getFactionRanks().begin()->first; - if (player.getClass().getNpcStats(player).isSameFaction(victim.getClass().getNpcStats(victim))) - { - player.getClass().getNpcStats(player).expell(factionID); - } - } - // Make surrounding actors within alarm distance respond to the crime std::vector neighbors; @@ -1045,19 +1147,25 @@ namespace MWMechanics // What amount of provocation did this crime generate? // Controls whether witnesses will engage combat with the criminal. - int fight = 0; + int fight = 0, fightVictim = 0; if (type == OT_Trespassing || type == OT_SleepingInOwnedBed) - fight = esmStore.get().find("iFightTrespass")->getInt(); + fight = fightVictim = esmStore.get().find("iFightTrespass")->getInt(); else if (type == OT_Pickpocket) + { fight = esmStore.get().find("iFightPickpocket")->getInt(); - else if (type == OT_Assault) // Note: iFightAttack is for the victim, iFightAttacking for witnesses? - fight = esmStore.get().find("iFightAttack")->getInt(); + fightVictim = esmStore.get().find("iFightPickpocket")->getInt() * 4; // *4 according to research wiki + } + else if (type == OT_Assault) + { + fight = esmStore.get().find("iFightAttacking")->getInt(); + fightVictim = esmStore.get().find("iFightAttack")->getInt(); + } else if (type == OT_Murder) - fight = esmStore.get().find("iFightKilling")->getInt(); + fight = fightVictim = esmStore.get().find("iFightKilling")->getInt(); else if (type == OT_Theft) - fight = esmStore.get().find("fFightStealing")->getFloat(); + fight = fightVictim = esmStore.get().find("fFightStealing")->getFloat(); - const int iFightAttacking = esmStore.get().find("iFightAttacking")->getInt(); + bool reported = false; // Tell everyone (including the original reporter) in alarm range for (std::vector::iterator it = neighbors.begin(); it != neighbors.end(); ++it) @@ -1065,11 +1173,14 @@ namespace MWMechanics if ( *it == player || !it->getClass().isNpc() || it->getClass().getCreatureStats(*it).isDead()) continue; - int aggression = fight; + if (it->getClass().getCreatureStats(*it).getAiSequence().isInCombat(victim)) + continue; - // Note: iFightAttack is used for the victim, iFightAttacking for witnesses? - if (*it != victim && type == OT_Assault) - aggression = iFightAttacking; + // Will the witness report the crime? + if (it->getClass().getCreatureStats(*it).getAiSetting(CreatureStats::AI_Alarm).getBase() >= 100) + { + reported = true; + } if (it->getClass().isClass(*it, "guard")) { @@ -1084,32 +1195,135 @@ namespace MWMechanics } else { - bool aggressive = MWBase::Environment::get().getMechanicsManager()->isAggressive(*it, player, aggression, true); - if (aggressive) + float dispTerm = (*it == victim) ? dispVictim : disp; + + float alarmTerm = 0.01 * it->getClass().getCreatureStats(*it).getAiSetting(CreatureStats::AI_Alarm).getBase(); + if (type == OT_Pickpocket && alarmTerm <= 0) + alarmTerm = 1.0; + + if (*it != victim) + dispTerm *= alarmTerm; + + float fightTerm = (*it == victim) ? fightVictim : fight; + fightTerm += getFightDispositionBias(dispTerm); + fightTerm += getFightDistanceBias(*it, player); + fightTerm *= alarmTerm; + + int observerFightRating = it->getClass().getCreatureStats(*it).getAiSetting(CreatureStats::AI_Fight).getBase(); + if (observerFightRating + fightTerm > 100) + fightTerm = 100 - observerFightRating; + fightTerm = std::max(0.f, fightTerm); + + if (observerFightRating + fightTerm >= 100) { startCombat(*it, player); + NpcStats& observerStats = it->getClass().getNpcStats(*it); + // Apply aggression value to the base Fight rating, so that the actor can continue fighting + // after a Calm spell wears off + observerStats.setAiSetting(CreatureStats::AI_Fight, observerFightRating + fightTerm); + + observerStats.setBaseDisposition(observerStats.getBaseDisposition()+dispTerm); + // Set the crime ID, which we will use to calm down participants // once the bounty has been paid. - it->getClass().getNpcStats(*it).setCrimeId(id); + observerStats.setCrimeId(id); // Mark as Alarmed for dialogue - it->getClass().getCreatureStats(*it).setAlarmed(true); + observerStats.setAlarmed(true); + } + } + } + + if (reported) + { + MWBase::Environment::get().getWindowManager()->messageBox("#{sCrimeMessage}"); + player.getClass().getNpcStats(player).setBounty(player.getClass().getNpcStats(player).getBounty() + + arg); + + // If committing a crime against a faction member, expell from the faction + if (!victim.isEmpty() && victim.getClass().isNpc()) + { + std::string factionID = victim.getClass().getPrimaryFaction(victim); + + const std::map& playerRanks = player.getClass().getNpcStats(player).getFactionRanks(); + if (playerRanks.find(Misc::StringUtils::lowerCase(factionID)) != playerRanks.end()) + { + player.getClass().getNpcStats(player).expell(factionID); } } + + if (type == OT_Assault && !victim.isEmpty() + && !victim.getClass().getCreatureStats(victim).getAiSequence().isInCombat(player) + && victim.getClass().isNpc()) + { + // Attacker is in combat with us, but we are not in combat with the attacker yet. Time to fight back. + // Note: accidental or collateral damage attacks are ignored. + startCombat(victim, player); + + // Set the crime ID, which we will use to calm down participants + // once the bounty has been paid. + victim.getClass().getNpcStats(victim).setCrimeId(id); + } } } + bool MechanicsManager::actorAttacked(const MWWorld::Ptr &ptr, const MWWorld::Ptr &attacker) + { + if (ptr == MWBase::Environment::get().getWorld()->getPlayerPtr()) + return false; + + std::list followers = getActorsFollowing(attacker); + if (std::find(followers.begin(), followers.end(), ptr) != followers.end()) + { + ptr.getClass().getCreatureStats(ptr).friendlyHit(); + + if (ptr.getClass().getCreatureStats(ptr).getFriendlyHits() < 4) + { + MWBase::Environment::get().getDialogueManager()->say(ptr, "hit"); + return false; + } + } + + // Attacking an NPC that is already in combat with any other NPC is not a crime + AiSequence& seq = ptr.getClass().getCreatureStats(ptr).getAiSequence(); + bool isFightingNpc = false; + for (std::list::const_iterator it = seq.begin(); it != seq.end(); ++it) + { + if ((*it)->getTypeId() == AiPackage::TypeIdCombat) + { + MWWorld::Ptr target = static_cast(*it)->getTarget(); + if (!target.isEmpty() && target.getClass().isNpc()) + isFightingNpc = true; + } + } + + if (ptr.getClass().isNpc() && !attacker.isEmpty() && !ptr.getClass().getCreatureStats(ptr).getAiSequence().isInCombat(attacker) + && !isAggressive(ptr, attacker) && !isFightingNpc) + commitCrime(attacker, ptr, MWBase::MechanicsManager::OT_Assault); + + if (!attacker.isEmpty() && (attacker.getClass().getCreatureStats(attacker).getAiSequence().isInCombat(ptr) + || attacker == MWBase::Environment::get().getWorld()->getPlayerPtr()) + && !ptr.getClass().getCreatureStats(ptr).getAiSequence().isInCombat(attacker)) + { + // Attacker is in combat with us, but we are not in combat with the attacker yet. Time to fight back. + // Note: accidental or collateral damage attacks are ignored. + startCombat(ptr, attacker); + } + + return true; + } + bool MechanicsManager::awarenessCheck(const MWWorld::Ptr &ptr, const MWWorld::Ptr &observer) { - if (observer.getClass().getCreatureStats(observer).isDead()) + if (observer.getClass().getCreatureStats(observer).isDead() || !observer.getRefData().isEnabled()) return false; const MWWorld::Store& store = MWBase::Environment::get().getWorld()->getStore().get(); CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); - float invisibility = stats.getMagicEffects().get(ESM::MagicEffect::Invisibility).mMagnitude; + float invisibility = stats.getMagicEffects().get(ESM::MagicEffect::Invisibility).getMagnitude(); if (invisibility > 0) return false; @@ -1141,13 +1355,13 @@ namespace MWMechanics Ogre::Vector3 pos2 (observer.getRefData().getPosition().pos); float distTerm = fSneakDistBase + fSneakDistMult * pos1.distance(pos2); - float chameleon = stats.getMagicEffects().get(ESM::MagicEffect::Chameleon).mMagnitude; + float chameleon = stats.getMagicEffects().get(ESM::MagicEffect::Chameleon).getMagnitude(); float x = sneakTerm * distTerm * stats.getFatigueTerm() + chameleon + invisibility; CreatureStats& observerStats = observer.getClass().getCreatureStats(observer); int obsAgility = observerStats.getAttribute(ESM::Attribute::Agility).getModified(); int obsLuck = observerStats.getAttribute(ESM::Attribute::Luck).getModified(); - float obsBlind = observerStats.getMagicEffects().get(ESM::MagicEffect::Blind).mMagnitude; + float obsBlind = observerStats.getMagicEffects().get(ESM::MagicEffect::Blind).getMagnitude(); int obsSneak = observer.getClass().getSkill(observer, ESM::Skill::Sneak); float obsTerm = obsSneak + 0.2 * obsAgility + 0.1 * obsLuck - obsBlind; @@ -1171,15 +1385,15 @@ namespace MWMechanics void MechanicsManager::startCombat(const MWWorld::Ptr &ptr, const MWWorld::Ptr &target) { + if (ptr.getClass().getCreatureStats(ptr).getAiSequence().isInCombat(target)) + return; ptr.getClass().getCreatureStats(ptr).getAiSequence().stack(MWMechanics::AiCombat(target), ptr); if (target == MWBase::Environment::get().getWorld()->getPlayerPtr()) { - ptr.getClass().getCreatureStats(ptr).setHostile(true); - // if guard starts combat with player, guards pursuing player should do the same if (ptr.getClass().isClass(ptr, "Guard")) { - for (Actors::PtrControllerMap::const_iterator iter = mActors.begin(); iter != mActors.end(); ++iter) + for (Actors::PtrActorMap::const_iterator iter = mActors.begin(); iter != mActors.end(); ++iter) { if (iter->first.getClass().isClass(iter->first, "Guard")) { @@ -1215,55 +1429,93 @@ namespace MWMechanics return mActors.getActorsFollowing(actor); } + std::list MechanicsManager::getActorsFollowingIndices(const MWWorld::Ptr& actor) + { + return mActors.getActorsFollowingIndices(actor); + } + std::list MechanicsManager::getActorsFighting(const MWWorld::Ptr& actor) { return mActors.getActorsFighting(actor); } int MechanicsManager::countSavedGameRecords() const { - return 1; // Death counter + return 1 // Death counter + +1; // Stolen items } void MechanicsManager::write(ESM::ESMWriter &writer, Loading::Listener &listener) const { mActors.write(writer, listener); + + ESM::StolenItems items; + items.mStolenItems = mStolenItems; + writer.startRecord(ESM::REC_STLN); + items.write(writer); + writer.endRecord(ESM::REC_STLN); } - void MechanicsManager::readRecord(ESM::ESMReader &reader, int32_t type) + void MechanicsManager::readRecord(ESM::ESMReader &reader, uint32_t type) { - mActors.readRecord(reader, type); + if (type == ESM::REC_STLN) + { + ESM::StolenItems items; + items.load(reader); + mStolenItems = items.mStolenItems; + } + else + mActors.readRecord(reader, type); } void MechanicsManager::clear() { mActors.clear(); + mStolenItems.clear(); } - bool MechanicsManager::isAggressive(const MWWorld::Ptr &ptr, const MWWorld::Ptr &target, int bias, bool ignoreDistance) + bool MechanicsManager::isAggressive(const MWWorld::Ptr &ptr, const MWWorld::Ptr &target) { - Ogre::Vector3 pos1 (ptr.getRefData().getPosition().pos); - Ogre::Vector3 pos2 (target.getRefData().getPosition().pos); - - float d = 0; - if (!ignoreDistance) - d = pos1.distance(pos2); - - static int iFightDistanceBase = MWBase::Environment::get().getWorld()->getStore().get().find( - "iFightDistanceBase")->getInt(); - static float fFightDistanceMultiplier = MWBase::Environment::get().getWorld()->getStore().get().find( - "fFightDistanceMultiplier")->getFloat(); - static float fFightDispMult = MWBase::Environment::get().getWorld()->getStore().get().find( - "fFightDispMult")->getFloat(); - int disposition = 50; if (ptr.getClass().isNpc()) disposition = getDerivedDisposition(ptr); int fight = std::max(0.f, ptr.getClass().getCreatureStats(ptr).getAiSetting(CreatureStats::AI_Fight).getModified() - + (iFightDistanceBase - fFightDistanceMultiplier * d) - + ((50 - disposition) * fFightDispMult)) - + bias; + + getFightDistanceBias(ptr, target) + + getFightDispositionBias(disposition)); + + if (ptr.getClass().isNpc() && target.getClass().isNpc()) + { + if (target.getClass().getNpcStats(target).isWerewolf() || + (target == MWBase::Environment::get().getWorld()->getPlayerPtr() && + MWBase::Environment::get().getWorld()->getGlobalInt("pcknownwerewolf"))) + { + const ESM::GameSetting * iWerewolfFightMod = MWBase::Environment::get().getWorld()->getStore().get().search("iWerewolfFightMod"); + fight += iWerewolfFightMod->getInt(); + } + } return (fight >= 100); } + + void MechanicsManager::keepPlayerAlive() + { + MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); + CreatureStats& stats = player.getClass().getCreatureStats(player); + if (stats.isDead()) + { + MWMechanics::DynamicStat stat (stats.getHealth()); + + if (stat.getModified()<1) + { + stat.setModified(1, 0); + stats.setHealth(stat); + } + stats.resurrect(); + } + } + + bool MechanicsManager::isReadyToBlock(const MWWorld::Ptr &ptr) const + { + return mActors.isReadyToBlock(ptr); + } } diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp index 365e24067..d08334ae8 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp @@ -26,6 +26,7 @@ namespace MWMechanics { MWWorld::Ptr mWatched; NpcStats mWatchedStats; + bool mWatchedStatsEmpty; bool mUpdatePlayer; bool mClassSelected; bool mRaceSelected; @@ -34,6 +35,11 @@ namespace MWMechanics Objects mObjects; Actors mActors; + typedef std::pair Owner; // < Owner id, bool isFaction > + typedef std::map OwnerMap; // < Owner, number of stolen items with this id from this owner > + typedef std::map StolenItemsMap; + StolenItemsMap mStolenItems; + public: void buildPlayer(); @@ -109,18 +115,20 @@ namespace MWMechanics virtual void startCombat (const MWWorld::Ptr& ptr, const MWWorld::Ptr& target); /** - * @brief Commit a crime. If any actors witness the crime and report it, - * reportCrime will be called automatically. * @note victim may be empty * @param arg Depends on \a type, e.g. for Theft, the value of the item that was stolen. - * @return was the crime reported? + * @param victimAware Is the victim already aware of the crime? + * If this parameter is false, it will be determined by a line-of-sight and awareness check. + * @return was the crime seen? */ virtual bool commitCrime (const MWWorld::Ptr& ptr, const MWWorld::Ptr& victim, - OffenseType type, int arg=0); - virtual void reportCrime (const MWWorld::Ptr& ptr, const MWWorld::Ptr& victim, - OffenseType type, int arg=0); + OffenseType type, int arg=0, bool victimAware=false); + /// @return false if the attack was considered a "friendly hit" and forgiven + virtual bool actorAttacked (const MWWorld::Ptr& victim, const MWWorld::Ptr& attacker); /// Utility to check if taking this item is illegal and calling commitCrime if so - virtual void itemTaken (const MWWorld::Ptr& ptr, const MWWorld::Ptr& item, int count); + /// @param container The container the item is in; may be empty for an item in the world + virtual void itemTaken (const MWWorld::Ptr& ptr, const MWWorld::Ptr& item, const MWWorld::Ptr& container, + int count); /// Utility to check if opening (i.e. unlocking) this object is illegal and calling commitCrime if so virtual void objectOpened (const MWWorld::Ptr& ptr, const MWWorld::Ptr& item); /// Attempt sleeping in a bed. If this is illegal, call commitCrime. @@ -141,6 +149,7 @@ namespace MWMechanics virtual void getActorsInRange(const Ogre::Vector3 &position, float radius, std::vector &objects); virtual std::list getActorsFollowing(const MWWorld::Ptr& actor); + virtual std::list getActorsFollowingIndices(const MWWorld::Ptr& actor); virtual std::list getActorsFighting(const MWWorld::Ptr& actor); @@ -153,17 +162,31 @@ namespace MWMechanics virtual void write (ESM::ESMWriter& writer, Loading::Listener& listener) const; - virtual void readRecord (ESM::ESMReader& reader, int32_t type); + virtual void readRecord (ESM::ESMReader& reader, uint32_t type); virtual void clear(); - /// @param bias Can be used to add an additional aggression bias towards the target, - /// making it more likely for the function to return true. - virtual bool isAggressive (const MWWorld::Ptr& ptr, const MWWorld::Ptr& target, int bias=0, bool ignoreDistance=false); + virtual bool isAggressive (const MWWorld::Ptr& ptr, const MWWorld::Ptr& target); + + virtual void keepPlayerAlive(); + + virtual bool isReadyToBlock (const MWWorld::Ptr& ptr) const; + + virtual void confiscateStolenItems (const MWWorld::Ptr& player, const MWWorld::Ptr& targetContainer); + + /// List the owners that the player has stolen this item from (the owner can be an NPC or a faction). + /// + virtual std::vector > getStolenItemOwners(const std::string& itemid); + + /// Has the player stolen this item from the given owner? + virtual bool isItemStolenFrom(const std::string& itemid, const std::string& ownerid); + + private: + void reportCrime (const MWWorld::Ptr& ptr, const MWWorld::Ptr& victim, + OffenseType type, int arg=0); - /// Usually done once a frame, but can be invoked manually in time-critical situations. - /// This will increase the death count for any actors that were killed. - virtual void killDeadActors(); + /// @return is \a ptr allowed to take/use \a cellref or is it a crime? + virtual bool isAllowedToUse (const MWWorld::Ptr& ptr, const MWWorld::CellRef& cellref, MWWorld::Ptr& victim); }; } diff --git a/apps/openmw/mwmechanics/npcstats.cpp b/apps/openmw/mwmechanics/npcstats.cpp index 579969f9d..58bf11cff 100644 --- a/apps/openmw/mwmechanics/npcstats.cpp +++ b/apps/openmw/mwmechanics/npcstats.cpp @@ -31,10 +31,7 @@ MWMechanics::NpcStats::NpcStats() , mReputation(0) , mCrimeId(-1) , mWerewolfKills (0) -, mProfit(0) , mTimeToStartDrowning(20.0) -, mLastDrowningHit(0) -, mLevelHealthBonus(0) { mSkillIncreases.resize (ESM::Attribute::Length, 0); } @@ -70,9 +67,35 @@ const std::map& MWMechanics::NpcStats::getFactionRanks() const return mFactionRank; } -std::map& MWMechanics::NpcStats::getFactionRanks() +void MWMechanics::NpcStats::raiseRank(const std::string &faction) { - return mFactionRank; + const std::string lower = Misc::StringUtils::lowerCase(faction); + std::map::iterator it = mFactionRank.find(lower); + if (it != mFactionRank.end()) + { + // Does the next rank exist? + const ESM::Faction* faction = MWBase::Environment::get().getWorld()->getStore().get().find(lower); + if (it->second+1 < 10 && !faction->mRanks[it->second+1].empty()) + it->second += 1; + } +} + +void MWMechanics::NpcStats::lowerRank(const std::string &faction) +{ + const std::string lower = Misc::StringUtils::lowerCase(faction); + std::map::iterator it = mFactionRank.find(lower); + if (it != mFactionRank.end()) + { + it->second = std::max(0, it->second-1); + } +} + +void MWMechanics::NpcStats::joinFaction(const std::string& faction) +{ + const std::string lower = Misc::StringUtils::lowerCase(faction); + std::map::iterator it = mFactionRank.find(lower); + if (it == mFactionRank.end()) + mFactionRank[lower] = 0; } bool MWMechanics::NpcStats::getExpelled(const std::string& factionID) const @@ -97,40 +120,29 @@ void MWMechanics::NpcStats::clearExpelled(const std::string& factionID) mExpelled.erase(Misc::StringUtils::lowerCase(factionID)); } -bool MWMechanics::NpcStats::isSameFaction (const NpcStats& npcStats) const +bool MWMechanics::NpcStats::isInFaction (const std::string& faction) const { - for (std::map::const_iterator iter (mFactionRank.begin()); iter!=mFactionRank.end(); - ++iter) - if (npcStats.mFactionRank.find (iter->first)!=npcStats.mFactionRank.end()) - return true; - - return false; + return (mFactionRank.find(Misc::StringUtils::lowerCase(faction)) != mFactionRank.end()); } -float MWMechanics::NpcStats::getSkillGain (int skillIndex, const ESM::Class& class_, int usageType, - int level) const +int MWMechanics::NpcStats::getFactionReputation (const std::string& faction) const { - if (level<0) - level = static_cast (getSkill (skillIndex).getBase()); + std::map::const_iterator iter = mFactionReputation.find (Misc::StringUtils::lowerCase(faction)); - const ESM::Skill *skill = - MWBase::Environment::get().getWorld()->getStore().get().find (skillIndex); - - float skillFactor = 1; - - if (usageType>=4) - throw std::runtime_error ("skill usage type out of range"); + if (iter==mFactionReputation.end()) + return 0; - if (usageType>=0) - { - skillFactor = skill->mData.mUseValue[usageType]; + return iter->second; +} - if (skillFactor<0) - throw std::runtime_error ("invalid skill gain factor"); +void MWMechanics::NpcStats::setFactionReputation (const std::string& faction, int value) +{ + mFactionReputation[Misc::StringUtils::lowerCase(faction)] = value; +} - if (skillFactor==0) - return 0; - } +float MWMechanics::NpcStats::getSkillProgressRequirement (int skillIndex, const ESM::Class& class_) const +{ + float progressRequirement = 1 + getSkill (skillIndex).getBase(); const MWWorld::Store &gmst = MWBase::Environment::get().getWorld()->getStore().get(); @@ -153,11 +165,15 @@ float MWMechanics::NpcStats::getSkillGain (int skillIndex, const ESM::Class& cla break; } + progressRequirement *= typeFactor; + if (typeFactor<=0) throw std::runtime_error ("invalid skill type factor"); float specialisationFactor = 1; + const ESM::Skill *skill = + MWBase::Environment::get().getWorld()->getStore().get().find (skillIndex); if (skill->mData.mSpecialization==class_.mData.mSpecialization) { specialisationFactor = gmst.find ("fSpecialSkillBonus")->getFloat(); @@ -165,22 +181,37 @@ float MWMechanics::NpcStats::getSkillGain (int skillIndex, const ESM::Class& cla if (specialisationFactor<=0) throw std::runtime_error ("invalid skill specialisation factor"); } - return 1.0 / ((level+1) * (1.0/skillFactor) * typeFactor * specialisationFactor); + progressRequirement *= specialisationFactor; + + return progressRequirement; } -void MWMechanics::NpcStats::useSkill (int skillIndex, const ESM::Class& class_, int usageType) +void MWMechanics::NpcStats::useSkill (int skillIndex, const ESM::Class& class_, int usageType, float extraFactor) { // Don't increase skills as a werewolf if(mIsWerewolf) return; + const ESM::Skill *skill = + MWBase::Environment::get().getWorld()->getStore().get().find (skillIndex); + float skillGain = 1; + if (usageType>=4) + throw std::runtime_error ("skill usage type out of range"); + if (usageType>=0) + { + skillGain = skill->mData.mUseValue[usageType]; + if (skillGain<0) + throw std::runtime_error ("invalid skill gain factor"); + } + skillGain *= extraFactor; + MWMechanics::SkillValue& value = getSkill (skillIndex); - value.setProgress(value.getProgress() + getSkillGain (skillIndex, class_, usageType)); + value.setProgress(value.getProgress() + skillGain); - if (value.getProgress()>=1) + if (int(value.getProgress())>=int(getSkillProgressRequirement(skillIndex, class_))) { - // skill leveled up + // skill levelled up increaseSkill(skillIndex, class_, false); } } @@ -224,21 +255,19 @@ void MWMechanics::NpcStats::increaseSkill(int skillIndex, const ESM::Class &clas /// \todo check if character is the player, if levelling is ever implemented for NPCs MWBase::Environment::get().getSoundManager ()->playSound ("skillraise", 1, 1); - std::vector noButtons; - std::stringstream message; message << boost::format(MWBase::Environment::get().getWindowManager ()->getGameSettingString ("sNotifyMessage39", "")) % std::string("#{" + ESM::Skill::sSkillNameIds[skillIndex] + "}") % static_cast (base); - MWBase::Environment::get().getWindowManager ()->messageBox(message.str(), noButtons, MWGui::ShowInDialogueMode_Never); + MWBase::Environment::get().getWindowManager ()->messageBox(message.str(), MWGui::ShowInDialogueMode_Never); if (mLevelProgress >= gmst.find("iLevelUpTotal")->getInt()) { // levelup is possible now - MWBase::Environment::get().getWindowManager ()->messageBox ("#{sLevelUpMsg}", noButtons, MWGui::ShowInDialogueMode_Never); + MWBase::Environment::get().getWindowManager ()->messageBox ("#{sLevelUpMsg}", MWGui::ShowInDialogueMode_Never); } - getSkill (skillIndex).setBase (base); + getSkill(skillIndex).setBase (base); if (!preserveProgress) getSkill(skillIndex).setProgress(0); } @@ -250,20 +279,21 @@ int MWMechanics::NpcStats::getLevelProgress () const void MWMechanics::NpcStats::levelUp() { - mLevelProgress -= 10; - for (int i=0; i &gmst = MWBase::Environment::get().getWorld()->getStore().get(); + mLevelProgress -= gmst.find("iLevelUpTotal")->getInt(); + mLevelProgress = std::max(0, mLevelProgress); // might be necessary when levelup was invoked via console + + for (int i=0; igetFloat(); - updateHealth(); + setHealth(getHealth().getBase() + endurance * gmst.find("fLevelUpHealthEndMult")->getFloat()); setLevel(getLevel()+1); } @@ -273,7 +303,7 @@ void MWMechanics::NpcStats::updateHealth() const int endurance = getAttribute(ESM::Attribute::Endurance).getBase(); const int strength = getAttribute(ESM::Attribute::Strength).getBase(); - setHealth(static_cast (0.5 * (strength + endurance)) + mLevelHealthBonus); + setHealth(static_cast (0.5 * (strength + endurance))); } int MWMechanics::NpcStats::getLevelupAttributeMultiplier(int attribute) const @@ -304,31 +334,12 @@ bool MWMechanics::NpcStats::hasBeenUsed (const std::string& id) const int MWMechanics::NpcStats::getBounty() const { - if (mIsWerewolf) - return MWBase::Environment::get().getWorld()->getStore().get().find("iWereWolfBounty")->getInt(); - else - return mBounty; + return mBounty; } void MWMechanics::NpcStats::setBounty (int bounty) { - if (!mIsWerewolf) - mBounty = bounty; -} - -int MWMechanics::NpcStats::getFactionReputation (const std::string& faction) const -{ - std::map::const_iterator iter = mFactionReputation.find (faction); - - if (iter==mFactionReputation.end()) - return 0; - - return iter->second; -} - -void MWMechanics::NpcStats::setFactionReputation (const std::string& faction, int value) -{ - mFactionReputation[faction] = value; + mBounty = bounty; } int MWMechanics::NpcStats::getReputation() const @@ -434,16 +445,6 @@ void MWMechanics::NpcStats::addWerewolfKill() ++mWerewolfKills; } -int MWMechanics::NpcStats::getProfit() const -{ - return mProfit; -} - -void MWMechanics::NpcStats::modifyProfit(int diff) -{ - mProfit += diff; -} - float MWMechanics::NpcStats::getTimeToStartDrowning() const { return mTimeToStartDrowning; @@ -487,7 +488,6 @@ void MWMechanics::NpcStats::writeState (ESM::NpcStats& state) const state.mReputation = mReputation; state.mWerewolfKills = mWerewolfKills; - state.mProfit = mProfit; state.mLevelProgress = mLevelProgress; for (int i=0; ifirst] = iter->second.mRank; if (iter->second.mReputation) - mFactionReputation[iter->first] = iter->second.mReputation; + mFactionReputation[Misc::StringUtils::lowerCase(iter->first)] = iter->second.mReputation; } mDisposition = state.mDisposition; @@ -536,7 +534,6 @@ void MWMechanics::NpcStats::readState (const ESM::NpcStats& state) mBounty = state.mBounty; mReputation = state.mReputation; mWerewolfKills = state.mWerewolfKills; - mProfit = state.mProfit; mLevelProgress = state.mLevelProgress; for (int i=0; i #include -#include "stat.hpp" - #include "creaturestats.hpp" namespace ESM @@ -19,49 +17,34 @@ namespace ESM namespace MWMechanics { /// \brief Additional stats for NPCs - /// - /// \note For technical reasons the spell list and the currently selected spell is also handled by - /// CreatureStats, even though they are actually NPC stats. class NpcStats : public CreatureStats { - /// NPCs other than the player can only have one faction. But for the sake of consistency - /// we use the same data structure for the PC and the NPCs. - /// \note the faction key must be in lowercase - std::map mFactionRank; - int mDisposition; - SkillValue mSkill[ESM::Skill::Length]; + SkillValue mSkill[ESM::Skill::Length]; // SkillValue.mProgress used by the player only SkillValue mWerewolfSkill[ESM::Skill::Length]; - int mBounty; - std::set mExpelled; - std::map mFactionReputation; int mReputation; int mCrimeId; - int mWerewolfKills; - int mProfit; + // ----- used by the player only, maybe should be moved at some point ------- + int mBounty; + int mWerewolfKills; + /// Used for the player only; NPCs have maximum one faction defined in their NPC record + std::map mFactionRank; + std::set mExpelled; + std::map mFactionReputation; int mLevelProgress; // 0-10 - std::vector mSkillIncreases; // number of skill increases for each attribute - std::set mUsedIds; + // --------------------------------------------------------------------------- /// Countdown to getting damage while underwater float mTimeToStartDrowning; - /// time since last hit from drowning - float mLastDrowningHit; - - float mLevelHealthBonus; public: NpcStats(); - /// for mercenary companions. starts out as 0, and changes when items are added or removed through the UI. - int getProfit() const; - void modifyProfit(int diff); - int getBaseDisposition() const; void setBaseDisposition(int disposition); @@ -75,23 +58,23 @@ namespace MWMechanics SkillValue& getSkill (int index); const std::map& getFactionRanks() const; - std::map& getFactionRanks(); + /// Increase the rank in this faction by 1, if such a rank exists. + void raiseRank(const std::string& faction); + /// Lower the rank in this faction by 1, if such a rank exists. + void lowerRank(const std::string& faction); + /// Join this faction, setting the initial rank to 0. + void joinFaction(const std::string& faction); const std::set& getExpelled() const { return mExpelled; } bool getExpelled(const std::string& factionID) const; void expell(const std::string& factionID); void clearExpelled(const std::string& factionID); - bool isSameFaction (const NpcStats& npcStats) const; - ///< Do *this and \a npcStats share a faction? + bool isInFaction (const std::string& faction) const; - float getSkillGain (int skillIndex, const ESM::Class& class_, int usageType = -1, - int level = -1) const; - ///< \param usageType: Usage specific factor, specified in the respective skill record; - /// -1: use a factor of 1.0 instead. - /// \param level Level to base calculation on; -1: use current level. + float getSkillProgressRequirement (int skillIndex, const ESM::Class& class_) const; - void useSkill (int skillIndex, const ESM::Class& class_, int usageType = -1); + void useSkill (int skillIndex, const ESM::Class& class_, int usageType = -1, float extraFactor=1.f); ///< Increase skill by usage. void increaseSkill (int skillIndex, const ESM::Class& class_, bool preserveProgress); @@ -104,11 +87,13 @@ namespace MWMechanics void updateHealth(); ///< Calculate health based on endurance and strength. - /// Called at character creation and at level up. + /// Called at character creation. void flagAsUsed (const std::string& id); + ///< @note Id must be lower-case bool hasBeenUsed (const std::string& id) const; + ///< @note Id must be lower-case int getBounty() const; diff --git a/apps/openmw/mwmechanics/obstacle.cpp b/apps/openmw/mwmechanics/obstacle.cpp index 55ebfeab5..6bf81e861 100644 --- a/apps/openmw/mwmechanics/obstacle.cpp +++ b/apps/openmw/mwmechanics/obstacle.cpp @@ -115,8 +115,8 @@ namespace MWMechanics if(mDistSameSpot == -1) mDistSameSpot = DIST_SAME_SPOT * (cls.getSpeed(actor) / 150); - bool samePosition = (abs(pos.pos[0] - mPrevX) < mDistSameSpot) && - (abs(pos.pos[1] - mPrevY) < mDistSameSpot); + bool samePosition = (std::abs(pos.pos[0] - mPrevX) < mDistSameSpot) && + (std::abs(pos.pos[1] - mPrevY) < mDistSameSpot); // update position mPrevX = pos.pos[0]; mPrevY = pos.pos[1]; diff --git a/apps/openmw/mwmechanics/obstacle.hpp b/apps/openmw/mwmechanics/obstacle.hpp index 76ab9d029..e0ae9203d 100644 --- a/apps/openmw/mwmechanics/obstacle.hpp +++ b/apps/openmw/mwmechanics/obstacle.hpp @@ -1,10 +1,6 @@ #ifndef OPENMW_MECHANICS_OBSTACLE_H #define OPENMW_MECHANICS_OBSTACLE_H -//#include "../mwbase/world.hpp" -//#include "../mwworld/class.hpp" -#include "../mwworld/cellstore.hpp" - namespace MWWorld { class Ptr; diff --git a/apps/openmw/mwmechanics/pathfinding.cpp b/apps/openmw/mwmechanics/pathfinding.cpp index 62c1756c2..0634725a8 100644 --- a/apps/openmw/mwmechanics/pathfinding.cpp +++ b/apps/openmw/mwmechanics/pathfinding.cpp @@ -278,40 +278,17 @@ namespace MWMechanics const ESM::Pathgrid::Point &nextPoint = *mPath.begin(); float directionX = nextPoint.mX - x; float directionY = nextPoint.mY - y; - float directionResult = sqrt(directionX * directionX + directionY * directionY); - return Ogre::Radian(Ogre::Math::ACos(directionY / directionResult) * sgn(Ogre::Math::ASin(directionX / directionResult))).valueDegrees(); + return Ogre::Math::ATan2(directionX,directionY).valueDegrees(); } - // Used by AiCombat, use Euclidean distance - float PathFinder::getDistToNext(float x, float y, float z) - { - ESM::Pathgrid::Point nextPoint = *mPath.begin(); - return distance(nextPoint, x, y, z); - } - - bool PathFinder::checkWaypoint(float x, float y, float z) - { - if(mPath.empty()) - return true; - - ESM::Pathgrid::Point nextPoint = *mPath.begin(); - if(sqrDistanceZCorrected(nextPoint, x, y, z) < 64*64) - { - mPath.pop_front(); - if(mPath.empty()) mIsPathConstructed = false; - return true; - } - return false; - } - - bool PathFinder::checkPathCompleted(float x, float y, float z) + bool PathFinder::checkPathCompleted(float x, float y, float z, float tolerance) { if(mPath.empty()) return true; ESM::Pathgrid::Point nextPoint = *mPath.begin(); - if(sqrDistanceZCorrected(nextPoint, x, y, z) < 64*64) + if(sqrDistanceZCorrected(nextPoint, x, y, z) < tolerance*tolerance) { mPath.pop_front(); if(mPath.empty()) diff --git a/apps/openmw/mwmechanics/pathfinding.hpp b/apps/openmw/mwmechanics/pathfinding.hpp index 603a04f8c..7ba2d22ba 100644 --- a/apps/openmw/mwmechanics/pathfinding.hpp +++ b/apps/openmw/mwmechanics/pathfinding.hpp @@ -39,16 +39,11 @@ namespace MWMechanics void buildPath(const ESM::Pathgrid::Point &startPoint, const ESM::Pathgrid::Point &endPoint, const MWWorld::CellStore* cell, bool allowShortcuts = true); - bool checkPathCompleted(float x, float y, float z); - ///< \Returns true if the last point of the path has been reached. - - bool checkWaypoint(float x, float y, float z); - ///< \Returns true if a way point was reached + bool checkPathCompleted(float x, float y, float z, float tolerance=32.f); + ///< \Returns true if we are within \a tolerance units of the last path point. float getZAngleToNext(float x, float y) const; - float getDistToNext(float x, float y, float z); - bool isPathConstructed() const { return mIsPathConstructed; diff --git a/apps/openmw/mwmechanics/pathgrid.cpp b/apps/openmw/mwmechanics/pathgrid.cpp index 4983a4a4f..9e50af2b8 100644 --- a/apps/openmw/mwmechanics/pathgrid.cpp +++ b/apps/openmw/mwmechanics/pathgrid.cpp @@ -4,6 +4,7 @@ #include "../mwbase/environment.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwworld/esmstore.hpp" namespace { @@ -95,7 +96,7 @@ namespace MWMechanics * +----------------> * high cost */ - bool PathgridGraph::load(const ESM::Cell* cell) + bool PathgridGraph::load(const MWWorld::CellStore *cell) { if(!cell) return false; @@ -103,10 +104,9 @@ namespace MWMechanics if(mIsGraphConstructed) return true; - mCell = cell; - mIsExterior = cell->isExterior(); - mPathgrid = MWBase::Environment::get().getWorld()->getStore().get().search(*cell); - + mCell = cell->getCell(); + mIsExterior = cell->getCell()->isExterior(); + mPathgrid = MWBase::Environment::get().getWorld()->getStore().get().search(*cell->getCell()); if(!mPathgrid) return false; diff --git a/apps/openmw/mwmechanics/pathgrid.hpp b/apps/openmw/mwmechanics/pathgrid.hpp index 5d01dca00..2742957a6 100644 --- a/apps/openmw/mwmechanics/pathgrid.hpp +++ b/apps/openmw/mwmechanics/pathgrid.hpp @@ -21,7 +21,7 @@ namespace MWMechanics public: PathgridGraph(); - bool load(const ESM::Cell *cell); + bool load(const MWWorld::CellStore *cell); // returns true if end point is strongly connected (i.e. reachable // from start point) both start and end are pathgrid point indexes diff --git a/apps/openmw/mwmechanics/spellcasting.cpp b/apps/openmw/mwmechanics/spellcasting.cpp index 67da99200..b91ea9984 100644 --- a/apps/openmw/mwmechanics/spellcasting.cpp +++ b/apps/openmw/mwmechanics/spellcasting.cpp @@ -15,11 +15,13 @@ #include "../mwworld/player.hpp" #include "../mwworld/class.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwrender/animation.hpp" #include "magiceffects.hpp" #include "npcstats.hpp" +#include "summoning.hpp" namespace { @@ -54,6 +56,64 @@ namespace } } + void applyDynamicStatsEffect(int attribute, const MWWorld::Ptr& target, float magnitude) + { + MWMechanics::DynamicStat value = target.getClass().getCreatureStats(target).getDynamic(attribute); + value.setCurrent(value.getCurrent()+magnitude, attribute == 2); + target.getClass().getCreatureStats(target).setDynamic(attribute, value); + } + + // TODO: refactor the effect tick functions in Actors so they can be reused here + void applyInstantEffectTick(MWMechanics::EffectKey effect, const MWWorld::Ptr& target, float magnitude) + { + int effectId = effect.mId; + if (effectId == ESM::MagicEffect::DamageHealth) + { + applyDynamicStatsEffect(0, target, magnitude * -1); + } + else if (effectId == ESM::MagicEffect::RestoreHealth) + { + applyDynamicStatsEffect(0, target, magnitude); + } + else if (effectId == ESM::MagicEffect::DamageFatigue) + { + applyDynamicStatsEffect(2, target, magnitude * -1); + } + else if (effectId == ESM::MagicEffect::RestoreFatigue) + { + applyDynamicStatsEffect(2, target, magnitude); + } + else if (effectId == ESM::MagicEffect::DamageMagicka) + { + applyDynamicStatsEffect(1, target, magnitude * -1); + } + else if (effectId == ESM::MagicEffect::RestoreMagicka) + { + applyDynamicStatsEffect(1, target, magnitude); + } + else if (effectId == ESM::MagicEffect::DamageAttribute || effectId == ESM::MagicEffect::RestoreAttribute) + { + int attribute = effect.mArg; + MWMechanics::AttributeValue value = target.getClass().getCreatureStats(target).getAttribute(attribute); + if (effectId == ESM::MagicEffect::DamageAttribute) + value.damage(magnitude); + else + value.restore(magnitude); + target.getClass().getCreatureStats(target).setAttribute(attribute, value); + } + else if (effectId == ESM::MagicEffect::DamageSkill || effectId == ESM::MagicEffect::RestoreSkill) + { + if (target.getTypeName() != typeid(ESM::NPC).name()) + return; + int skill = effect.mArg; + MWMechanics::SkillValue& value = target.getClass().getNpcStats(target).getSkill(skill); + if (effectId == ESM::MagicEffect::DamageSkill) + value.damage(magnitude); + else + value.restore(magnitude); + } + } + } namespace MWMechanics @@ -72,11 +132,11 @@ namespace MWMechanics return schoolSkillMap[school]; } - float getSpellSuccessChance (const ESM::Spell* spell, const MWWorld::Ptr& actor, int* effectiveSchool) + float getSpellSuccessChance (const ESM::Spell* spell, const MWWorld::Ptr& actor, int* effectiveSchool, bool cap) { CreatureStats& stats = actor.getClass().getCreatureStats(actor); - if (stats.getMagicEffects().get(ESM::MagicEffect::Silence).mMagnitude) + if (stats.getMagicEffects().get(ESM::MagicEffect::Silence).getMagnitude()) return 0; float y = FLT_MAX; @@ -114,7 +174,7 @@ namespace MWMechanics if (spell->mData.mFlags & ESM::Spell::F_Always) return 100; - int castBonus = -stats.getMagicEffects().get(ESM::MagicEffect::Sound).mMagnitude; + int castBonus = -stats.getMagicEffects().get(ESM::MagicEffect::Sound).getMagnitude(); int actorWillpower = stats.getAttribute(ESM::Attribute::Willpower).getModified(); int actorLuck = stats.getAttribute(ESM::Attribute::Luck).getModified(); @@ -123,14 +183,17 @@ namespace MWMechanics if (MWBase::Environment::get().getWorld()->getGodModeState() && actor.getRefData().getHandle() == "player") castChance = 100; - return std::max(0.f, std::min(100.f, castChance)); + if (!cap) + return std::max(0.f, castChance); + else + return std::max(0.f, std::min(100.f, castChance)); } - float getSpellSuccessChance (const std::string& spellId, const MWWorld::Ptr& actor, int* effectiveSchool) + float getSpellSuccessChance (const std::string& spellId, const MWWorld::Ptr& actor, int* effectiveSchool, bool cap) { const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().find(spellId); - return getSpellSuccessChance(spell, actor, effectiveSchool); + return getSpellSuccessChance(spell, actor, effectiveSchool, cap); } @@ -148,6 +211,20 @@ namespace MWMechanics return school; } + bool spellIncreasesSkill(const ESM::Spell *spell) + { + if (spell->mData.mType == ESM::Spell::ST_Spell && !(spell->mData.mFlags & ESM::Spell::F_Always)) + return true; + return false; + } + + bool spellIncreasesSkill(const std::string &spellId) + { + const ESM::Spell* spell = + MWBase::Environment::get().getWorld()->getStore().get().find(spellId); + return spellIncreasesSkill(spell); + } + float getEffectResistanceAttribute (short effectId, const MagicEffects* actorEffects) { short resistanceEffect = ESM::MagicEffect::getResistanceEffect(effectId); @@ -155,16 +232,16 @@ namespace MWMechanics float resistance = 0; if (resistanceEffect != -1) - resistance += actorEffects->get(resistanceEffect).mMagnitude; + resistance += actorEffects->get(resistanceEffect).getMagnitude(); if (weaknessEffect != -1) - resistance -= actorEffects->get(weaknessEffect).mMagnitude; + resistance -= actorEffects->get(weaknessEffect).getMagnitude(); if (effectId == ESM::MagicEffect::FireDamage) - resistance += actorEffects->get(ESM::MagicEffect::FireShield).mMagnitude; + resistance += actorEffects->get(ESM::MagicEffect::FireShield).getMagnitude(); if (effectId == ESM::MagicEffect::ShockDamage) - resistance += actorEffects->get(ESM::MagicEffect::LightningShield).mMagnitude; + resistance += actorEffects->get(ESM::MagicEffect::LightningShield).getMagnitude(); if (effectId == ESM::MagicEffect::FrostDamage) - resistance += actorEffects->get(ESM::MagicEffect::FrostShield).mMagnitude; + resistance += actorEffects->get(ESM::MagicEffect::FrostShield).getMagnitude(); return resistance; } @@ -184,6 +261,10 @@ namespace MWMechanics float resisted = 0; if (magicEffect->mData.mFlags & ESM::MagicEffect::Harmful) { + // Effects with no resistance attribute belonging to them can not be resisted + if (ESM::MagicEffect::getResistanceEffect(effectId) == -1) + return 0.f; + float resistance = getEffectResistanceAttribute(effectId, magicEffects); float willpower = stats.getAttribute(ESM::Attribute::Willpower).getModified(); @@ -191,12 +272,13 @@ namespace MWMechanics float x = (willpower + 0.1 * luck) * stats.getFatigueTerm(); // This makes spells that are easy to cast harder to resist and vice versa + float castChance = 100.f; if (spell != NULL && !caster.isEmpty() && caster.getClass().isActor()) { - float castChance = getSpellSuccessChance(spell, caster); - if (castChance > 0) - x *= 50 / castChance; + castChance = getSpellSuccessChance(spell, caster, NULL, false); // Uncapped casting chance } + if (castChance > 0) + x *= 50 / castChance; float roll = static_cast(std::rand()) / RAND_MAX * 100; if (magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude) @@ -224,10 +306,45 @@ namespace MWMechanics const ESM::Spell* spell, const MagicEffects* effects) { float resistance = getEffectResistance(effectId, actor, caster, spell, effects); - if (resistance >= 0) - return 1 - resistance / 100.f; - else - return -(resistance-100) / 100.f; + return 1 - resistance / 100.f; + } + + /// Check if the given affect can be applied to the target. If \a castByPlayer, emits a message box on failure. + bool checkEffectTarget (int effectId, const MWWorld::Ptr& target, bool castByPlayer) + { + switch (effectId) + { + case ESM::MagicEffect::Levitate: + if (!MWBase::Environment::get().getWorld()->isLevitationEnabled()) + { + if (castByPlayer) + MWBase::Environment::get().getWindowManager()->messageBox("#{sLevitateDisabled}"); + return false; + } + break; + case ESM::MagicEffect::Soultrap: + if (!target.getClass().isNpc() // no messagebox for NPCs + && (target.getTypeName() == typeid(ESM::Creature).name() && target.get()->mBase->mData.mSoul == 0)) + { + if (castByPlayer) + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicInvalidTarget}"); + return true; // must still apply to get visual effect and have target regard it as attack + } + break; + case ESM::MagicEffect::AlmsiviIntervention: + case ESM::MagicEffect::DivineIntervention: + case ESM::MagicEffect::Mark: + case ESM::MagicEffect::Recall: + if (!MWBase::Environment::get().getWorld()->isTeleportingEnabled()) + { + if (castByPlayer) + MWBase::Environment::get().getWindowManager()->messageBox("#{sTeleportDisabled}"); + return false; + } + break; + } + + return true; } CastSpell::CastSpell(const MWWorld::Ptr &caster, const MWWorld::Ptr &target) @@ -235,6 +352,7 @@ namespace MWMechanics , mTarget(target) , mStack(false) , mHitPosition(0,0,0) + , mAlwaysSucceed(false) { } @@ -249,9 +367,11 @@ namespace MWMechanics for (std::vector::const_iterator iter (effects.mList.begin()); iter!=effects.mList.end(); ++iter) { - if (iter->mRange != range) - continue; - found = true; + if (iter->mRange == range) + { + found = true; + break; + } } if (!found) return; @@ -260,8 +380,8 @@ namespace MWMechanics if (spell && (spell->mData.mType == ESM::Spell::ST_Disease || spell->mData.mType == ESM::Spell::ST_Blight)) { float x = (spell->mData.mType == ESM::Spell::ST_Disease) ? - target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::ResistCommonDisease).mMagnitude - : target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::ResistBlightDisease).mMagnitude; + target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::ResistCommonDisease).getMagnitude() + : target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::ResistBlightDisease).getMagnitude(); int roll = std::rand()/ (static_cast (RAND_MAX) + 1) * 100; // [0, 99] if (roll <= x) @@ -287,6 +407,27 @@ namespace MWMechanics bool castByPlayer = (!caster.isEmpty() && caster.getRefData().getHandle() == "player"); + // Try absorbing if it's a spell + // NOTE: Vanilla does this once per effect source instead of adding the % from all sources together, not sure + // if that is worth replicating. + bool absorbed = false; + if (spell && caster != target && target.getClass().isActor()) + { + int absorb = target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::SpellAbsorption).getMagnitude(); + int roll = std::rand()/ (static_cast (RAND_MAX) + 1) * 100; // [0, 99] + absorbed = (roll < absorb); + if (absorbed) + { + const ESM::Static* absorbStatic = MWBase::Environment::get().getWorld()->getStore().get().find ("VFX_Absorb"); + MWBase::Environment::get().getWorld()->getAnimation(target)->addEffect( + "meshes\\" + absorbStatic->mModel, ESM::MagicEffect::SpellAbsorption, false, ""); + // Magicka is increased by cost of spell + DynamicStat magicka = target.getClass().getCreatureStats(target).getMagicka(); + magicka.setCurrent(magicka.getCurrent() + spell->mData.mCost); + target.getClass().getCreatureStats(target).setMagicka(magicka); + } + } + for (std::vector::const_iterator effectIt (effects.mList.begin()); effectIt!=effects.mList.end(); ++effectIt) { @@ -297,23 +438,8 @@ namespace MWMechanics MWBase::Environment::get().getWorld()->getStore().get().find ( effectIt->mEffectID); - if (!MWBase::Environment::get().getWorld()->isLevitationEnabled() && effectIt->mEffectID == ESM::MagicEffect::Levitate) - { - if (castByPlayer) - MWBase::Environment::get().getWindowManager()->messageBox("#{sLevitateDisabled}"); - continue; - } - - if (!MWBase::Environment::get().getWorld()->isTeleportingEnabled() && - (effectIt->mEffectID == ESM::MagicEffect::AlmsiviIntervention || - effectIt->mEffectID == ESM::MagicEffect::DivineIntervention || - effectIt->mEffectID == ESM::MagicEffect::Mark || - effectIt->mEffectID == ESM::MagicEffect::Recall)) - { - if (castByPlayer) - MWBase::Environment::get().getWindowManager()->messageBox("#{sTeleportDisabled}"); + if (!checkEffectTarget(effectIt->mEffectID, target, castByPlayer)) continue; - } // If player is healing someone, show the target's HP bar if (castByPlayer && target != caster @@ -326,35 +452,17 @@ namespace MWMechanics { anyHarmfulEffect = true; + if (absorbed) // Absorbed, and we know there was a harmful effect (figuring that out is the only reason we are in this loop) + break; + // If player is attempting to cast a harmful spell, show the target's HP bar if (castByPlayer && target != caster) MWBase::Environment::get().getWindowManager()->setEnemy(target); - // Try absorbing if it's a spell - // NOTE: Vanilla does this once per effect source instead of adding the % from all sources together, not sure - // if that is worth replicating. - if (spell && caster != target) - { - int absorb = target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::SpellAbsorption).mMagnitude; - int roll = std::rand()/ (static_cast (RAND_MAX) + 1) * 100; // [0, 99] - bool isAbsorbed = (roll < absorb); - if (isAbsorbed) - { - const ESM::Static* absorbStatic = MWBase::Environment::get().getWorld()->getStore().get().find ("VFX_Absorb"); - MWBase::Environment::get().getWorld()->getAnimation(target)->addEffect( - "meshes\\" + absorbStatic->mModel, ESM::MagicEffect::Reflect, false, ""); - // Magicka is increased by cost of spell - DynamicStat magicka = target.getClass().getCreatureStats(target).getMagicka(); - magicka.setCurrent(magicka.getCurrent() + spell->mData.mCost); - target.getClass().getCreatureStats(target).setMagicka(magicka); - magnitudeMult = 0; - } - } - // Try reflecting if (!reflected && magnitudeMult > 0 && !caster.isEmpty() && caster != target && !(magicEffect->mData.mFlags & ESM::MagicEffect::Unreflectable)) { - int reflect = target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::Reflect).mMagnitude; + int reflect = target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::Reflect).getMagnitude(); int roll = std::rand()/ (static_cast (RAND_MAX) + 1) * 100; // [0, 99] bool isReflected = (roll < reflect); if (isReflected) @@ -376,21 +484,20 @@ namespace MWMechanics // Fully resisted, show message if (target.getRefData().getHandle() == "player") MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicPCResisted}"); - else + else if (castByPlayer) MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicTargetResisted}"); } } } - - if (magnitudeMult > 0) + if (magnitudeMult > 0 && !absorbed) { float random = std::rand() / static_cast(RAND_MAX); float magnitude = effectIt->mMagnMin + (effectIt->mMagnMax - effectIt->mMagnMin) * random; magnitude *= magnitudeMult; bool hasDuration = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration); - if (target.getClass().isActor() && hasDuration) + if (target.getClass().isActor() && hasDuration && effectIt->mDuration > 0) { ActiveSpells::ActiveEffect effect; effect.mEffectId = effectIt->mEffectID; @@ -422,17 +529,24 @@ namespace MWMechanics } } else - applyInstantEffect(target, caster, EffectKey(*effectIt), magnitude); - - // HACK: Damage attribute/skill actually has a duration, even though the actual effect is instant and permanent. - // This was probably just done to have the effect visible in the magic menu for a while - // to notify the player they've been damaged? - if (effectIt->mEffectID == ESM::MagicEffect::DamageAttribute - || effectIt->mEffectID == ESM::MagicEffect::DamageSkill - || effectIt->mEffectID == ESM::MagicEffect::RestoreAttribute - || effectIt->mEffectID == ESM::MagicEffect::RestoreSkill - ) - applyInstantEffect(target, caster, EffectKey(*effectIt), magnitude); + { + if (hasDuration && target.getClass().isActor()) + applyInstantEffectTick(EffectKey(*effectIt), target, magnitude); + else + applyInstantEffect(target, caster, EffectKey(*effectIt), magnitude); + } + + // Re-casting a summon effect will remove the creature from previous castings of that effect. + if (isSummoningEffect(effectIt->mEffectID) && !target.isEmpty() && target.getClass().isActor()) + { + CreatureStats& targetStats = target.getClass().getCreatureStats(target); + std::map::iterator found = targetStats.getSummonedCreatureMap().find(std::make_pair(effectIt->mEffectID, mId)); + if (found != targetStats.getSummonedCreatureMap().end()) + { + cleanupSummonedCreature(targetStats, found->second); + targetStats.getSummonedCreatureMap().erase(found); + } + } if (target.getClass().isActor() || magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration) { @@ -469,10 +583,10 @@ namespace MWMechanics } if (!exploded) - MWBase::Environment::get().getWorld()->explodeSpell(mHitPosition, effects, caster, mId, mSourceName); + MWBase::Environment::get().getWorld()->explodeSpell(mHitPosition, effects, caster, range, mId, mSourceName); if (!reflectedEffects.mList.empty()) - inflict(caster, target, reflectedEffects, range, true); + inflict(caster, target, reflectedEffects, range, true, exploded); if (!appliedLastingEffects.empty()) { @@ -523,28 +637,6 @@ namespace MWMechanics } else { - if (effectId == ESM::MagicEffect::DamageAttribute || effectId == ESM::MagicEffect::RestoreAttribute) - { - int attribute = effect.mArg; - AttributeValue value = target.getClass().getCreatureStats(target).getAttribute(attribute); - if (effectId == ESM::MagicEffect::DamageAttribute) - value.damage(magnitude); - else - value.restore(magnitude); - target.getClass().getCreatureStats(target).setAttribute(attribute, value); - } - else if (effectId == ESM::MagicEffect::DamageSkill || effectId == ESM::MagicEffect::RestoreSkill) - { - if (target.getTypeName() != typeid(ESM::NPC).name()) - return; - int skill = effect.mArg; - SkillValue& value = target.getClass().getNpcStats(target).getSkill(skill); - if (effectId == ESM::MagicEffect::DamageSkill) - value.damage(magnitude); - else - value.restore(magnitude); - } - if (effectId == ESM::MagicEffect::CurePoison) target.getClass().getCreatureStats(target).getActiveSpells().purgeEffect(ESM::MagicEffect::Poison); else if (effectId == ESM::MagicEffect::CureParalyzation) @@ -595,6 +687,7 @@ namespace MWMechanics } } + bool CastSpell::cast(const std::string &id) { if (const ESM::Spell *spell = @@ -628,18 +721,29 @@ namespace MWMechanics // Check if there's enough charge left if (enchantment->mData.mType == ESM::Enchantment::WhenUsed || enchantment->mData.mType == ESM::Enchantment::WhenStrikes) { - const float enchantCost = enchantment->mData.mCost; - int eSkill = mCaster.getClass().getSkill(mCaster, ESM::Skill::Enchant); - const int castCost = std::max(1.f, enchantCost - (enchantCost / 100) * (eSkill - 10)); + const int castCost = getEffectiveEnchantmentCastCost(enchantment->mData.mCost, mCaster); if (item.getCellRef().getEnchantmentCharge() == -1) item.getCellRef().setEnchantmentCharge(enchantment->mData.mCharge); if (item.getCellRef().getEnchantmentCharge() < castCost) { - // TODO: Should there be a sound here? if (mCaster.getRefData().getHandle() == "player") MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicInsufficientCharge}"); + + // Failure sound + int school = 0; + if (!enchantment->mEffects.mList.empty()) + { + short effectId = enchantment->mEffects.mList.front().mEffectID; + const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find(effectId); + school = magicEffect->mData.mSchool; + } + static const std::string schools[] = { + "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration" + }; + MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); + sndMgr->playSound3D(mCaster, "Spell Failure " + schools[school], 1.0f, 1.0f); return false; } // Reduce charge @@ -665,8 +769,7 @@ namespace MWMechanics if (!mTarget.isEmpty()) { - if (!mTarget.getClass().isActor() || !mTarget.getClass().getCreatureStats(mTarget).isDead()) - inflict(mTarget, mCaster, enchantment->mEffects, ESM::RT_Touch); + inflict(mTarget, mCaster, enchantment->mEffects, ESM::RT_Touch); } std::string projectileModel; @@ -703,7 +806,7 @@ namespace MWMechanics int school = 0; - if (mCaster.getClass().isActor()) + if (mCaster.getClass().isActor() && !mAlwaysSucceed) { school = getSpellSchool(spell, mCaster); @@ -713,7 +816,7 @@ namespace MWMechanics static const float fFatigueSpellBase = store.get().find("fFatigueSpellBase")->getFloat(); static const float fFatigueSpellMult = store.get().find("fFatigueSpellMult")->getFloat(); DynamicStat fatigue = stats.getFatigue(); - const float normalizedEncumbrance = mCaster.getClass().getEncumbrance(mCaster) / mCaster.getClass().getCapacity(mCaster); + const float normalizedEncumbrance = mCaster.getClass().getNormalizedEncumbrance(mCaster); float fatigueLoss = spell->mData.mCost * (fFatigueSpellBase + normalizedEncumbrance * fFatigueSpellMult); fatigue.setCurrent(fatigue.getCurrent() - fatigueLoss); stats.setFatigue(fatigue); @@ -742,7 +845,7 @@ namespace MWMechanics } } - if (mCaster.getRefData().getHandle() == "player" && spell->mData.mType == ESM::Spell::ST_Spell) + if (mCaster.getRefData().getHandle() == "player" && spellIncreasesSkill(spell)) mCaster.getClass().skillUsageSucceeded(mCaster, spellSchoolToSkill(school), 0); @@ -750,10 +853,7 @@ namespace MWMechanics if (!mTarget.isEmpty()) { - if (!mTarget.getClass().isActor() || !mTarget.getClass().getCreatureStats(mTarget).isDead()) - { - inflict(mTarget, mCaster, spell->mEffects, ESM::RT_Touch); - } + inflict(mTarget, mCaster, spell->mEffects, ESM::RT_Touch); } @@ -842,4 +942,24 @@ namespace MWMechanics return true; } + int getEffectiveEnchantmentCastCost(float castCost, const MWWorld::Ptr &actor) + { + /* + * Each point of enchant skill above/under 10 subtracts/adds + * one percent of enchantment cost while minimum is 1. + */ + int eSkill = actor.getClass().getSkill(actor, ESM::Skill::Enchant); + const float result = castCost - (castCost / 100) * (eSkill - 10); + + return static_cast((result < 1) ? 1 : result); + } + + bool isSummoningEffect(int effectId) + { + return ((effectId >= ESM::MagicEffect::SummonScamp + && effectId <= ESM::MagicEffect::SummonStormAtronach) + || effectId == ESM::MagicEffect::SummonCenturionSphere + || (effectId >= ESM::MagicEffect::SummonFabricant + && effectId <= ESM::MagicEffect::SummonCreature05)); + } } diff --git a/apps/openmw/mwmechanics/spellcasting.hpp b/apps/openmw/mwmechanics/spellcasting.hpp index 2de187ae4..dca4f0192 100644 --- a/apps/openmw/mwmechanics/spellcasting.hpp +++ b/apps/openmw/mwmechanics/spellcasting.hpp @@ -22,19 +22,26 @@ namespace MWMechanics ESM::Skill::SkillEnum spellSchoolToSkill(int school); + bool isSummoningEffect(int effectId); + /** * @param spell spell to cast * @param actor calculate spell success chance for this actor (depends on actor's skills) * @param effectiveSchool the spell's effective school (relevant for skill progress) will be written here - * @attention actor has to be an NPC and not a creature! - * @return success chance from 0 to 100 (in percent) + * @param cap cap the result to 100%? + * @note actor can be an NPC or a creature + * @return success chance from 0 to 100 (in percent), if cap=false then chance above 100 may be returned. */ - float getSpellSuccessChance (const ESM::Spell* spell, const MWWorld::Ptr& actor, int* effectiveSchool = NULL); - float getSpellSuccessChance (const std::string& spellId, const MWWorld::Ptr& actor, int* effectiveSchool = NULL); + float getSpellSuccessChance (const ESM::Spell* spell, const MWWorld::Ptr& actor, int* effectiveSchool = NULL, bool cap=true); + float getSpellSuccessChance (const std::string& spellId, const MWWorld::Ptr& actor, int* effectiveSchool = NULL, bool cap=true); int getSpellSchool(const std::string& spellId, const MWWorld::Ptr& actor); int getSpellSchool(const ESM::Spell* spell, const MWWorld::Ptr& actor); + /// Get whether or not the given spell contributes to skill progress. + bool spellIncreasesSkill(const ESM::Spell* spell); + bool spellIncreasesSkill(const std::string& spellId); + /// Get the resistance attribute against an effect for a given actor. This will add together /// ResistX and Weakness to X effects relevant against the given effect. float getEffectResistanceAttribute (short effectId, const MagicEffects* actorEffects); @@ -46,9 +53,15 @@ namespace MWMechanics float getEffectResistance (short effectId, const MWWorld::Ptr& actor, const MWWorld::Ptr& caster, const ESM::Spell* spell = NULL, const MagicEffects* effects = NULL); + /// Get an effect multiplier for applying an effect cast by the given actor in the given spell (optional). + /// @return effect multiplier from 0 to 2. (100% net resistance to 100% net weakness) + /// @param effects Override the actor's current magicEffects. Useful if there are effects currently + /// being applied (but not applied yet) that should also be considered. float getEffectMultiplier(short effectId, const MWWorld::Ptr& actor, const MWWorld::Ptr& caster, const ESM::Spell* spell = NULL, const MagicEffects* effects = NULL); + int getEffectiveEnchantmentCastCost (float castCost, const MWWorld::Ptr& actor); + class CastSpell { private: @@ -59,6 +72,7 @@ namespace MWMechanics std::string mId; // ID of spell, potion, item etc std::string mSourceName; // Display name for spell, potion, etc Ogre::Vector3 mHitPosition; // Used for spawning area orb + bool mAlwaysSucceed; // Always succeed spells casted by NPCs/creatures regardless of their chance (default: false) public: CastSpell(const MWWorld::Ptr& caster, const MWWorld::Ptr& target); diff --git a/apps/openmw/mwmechanics/spells.cpp b/apps/openmw/mwmechanics/spells.cpp index dee1a1b05..dcbd3f09f 100644 --- a/apps/openmw/mwmechanics/spells.cpp +++ b/apps/openmw/mwmechanics/spells.cpp @@ -25,12 +25,10 @@ namespace MWMechanics return mSpells.end(); } - void Spells::add (const std::string& spellId) + void Spells::add (const ESM::Spell* spell) { - if (mSpells.find (spellId)==mSpells.end()) + if (mSpells.find (spell->mId)==mSpells.end()) { - const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().find(spellId); - std::map random; // Determine the random magnitudes (unless this is a castable spell, in which case @@ -44,14 +42,49 @@ namespace MWMechanics } } - mSpells.insert (std::make_pair (Misc::StringUtils::lowerCase(spellId), random)); + if (hasCorprusEffect(spell)) + { + CorprusStats corprus; + corprus.mWorsenings = 0; + corprus.mNextWorsening = MWBase::Environment::get().getWorld()->getTimeStamp() + CorprusStats::sWorseningPeriod; + + mCorprusSpells[spell->mId] = corprus; + } + + mSpells.insert (std::make_pair (spell->mId, random)); } } + void Spells::add (const std::string& spellId) + { + const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().find(spellId); + add(spell); + } + void Spells::remove (const std::string& spellId) { std::string lower = Misc::StringUtils::lowerCase(spellId); TContainer::iterator iter = mSpells.find (lower); + std::map::iterator corprusIt = mCorprusSpells.find(lower); + + // if it's corprus, remove negative and keep positive effects + if (corprusIt != mCorprusSpells.end()) + { + worsenCorprus(lower); + if (mPermanentSpellEffects.find(lower) != mPermanentSpellEffects.end()) + { + MagicEffects & effects = mPermanentSpellEffects[lower]; + for (MagicEffects::Collection::const_iterator effectIt = effects.begin(); effectIt != effects.end();) + { + const ESM::MagicEffect * magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find(effectIt->first.mId); + if (magicEffect->mData.mFlags & ESM::MagicEffect::Harmful) + effects.remove((effectIt++)->first); + else + ++effectIt; + } + } + mCorprusSpells.erase(corprusIt); + } if (iter!=mSpells.end()) mSpells.erase (iter); @@ -87,6 +120,11 @@ namespace MWMechanics } } + for (std::map::const_iterator it = mPermanentSpellEffects.begin(); it != mPermanentSpellEffects.end(); ++it) + { + effects += it->second; + } + return effects; } @@ -154,7 +192,7 @@ namespace MWMechanics const ESM::Spell *spell = MWBase::Environment::get().getWorld()->getStore().get().find (iter->first); - if (spell->mData.mType == ESM::Spell::ST_Blight) + if (spell->mData.mType == ESM::Spell::ST_Blight && !hasCorprusEffect(spell)) mSpells.erase(iter++); else ++iter; @@ -168,7 +206,7 @@ namespace MWMechanics const ESM::Spell *spell = MWBase::Environment::get().getWorld()->getStore().get().find (iter->first); - if (Misc::StringUtils::ciEqual(spell->mId, "corprus")) + if (hasCorprusEffect(spell)) mSpells.erase(iter++); else ++iter; @@ -211,9 +249,51 @@ namespace MWMechanics random = it->second.at(i); float magnitude = effectIt->mMagnMin + (effectIt->mMagnMax - effectIt->mMagnMin) * random; - visitor.visit(MWMechanics::EffectKey(*effectIt), spell->mName, -1, magnitude); + visitor.visit(MWMechanics::EffectKey(*effectIt), spell->mName, spell->mId, -1, magnitude); + } + } + } + + void Spells::worsenCorprus(const std::string &corpSpellId) + { + mCorprusSpells[corpSpellId].mNextWorsening = MWBase::Environment::get().getWorld()->getTimeStamp() + CorprusStats::sWorseningPeriod; + mCorprusSpells[corpSpellId].mWorsenings++; + + // update worsened effects + mPermanentSpellEffects[corpSpellId] = MagicEffects(); + const ESM::Spell * spell = MWBase::Environment::get().getWorld()->getStore().get().find(corpSpellId); + int i=0; + for (std::vector::const_iterator effectIt = spell->mEffects.mList.begin(); effectIt != spell->mEffects.mList.end(); ++effectIt, ++i) + { + const ESM::MagicEffect * magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find(effectIt->mEffectID); + if ((effectIt->mEffectID != ESM::MagicEffect::Corprus) && (magicEffect->mData.mFlags & ESM::MagicEffect::UncappedDamage)) // APPLIED_ONCE + { + float random = 1.f; + if (mSpells[corpSpellId].find(i) != mSpells[corpSpellId].end()) + random = mSpells[corpSpellId].at(i); + + float magnitude = effectIt->mMagnMin + (effectIt->mMagnMax - effectIt->mMagnMin) * random; + magnitude *= std::max(1, mCorprusSpells[corpSpellId].mWorsenings); + mPermanentSpellEffects[corpSpellId].add(MWMechanics::EffectKey(*effectIt), MWMechanics::EffectParam(magnitude)); + } + } + } + + bool Spells::hasCorprusEffect(const ESM::Spell *spell) + { + for (std::vector::const_iterator effectIt = spell->mEffects.mList.begin(); effectIt != spell->mEffects.mList.end(); ++effectIt) + { + if (effectIt->mEffectID == ESM::MagicEffect::Corprus) + { + return true; } } + return false; + } + + const std::map &Spells::getCorprusSpells() const + { + return mCorprusSpells; } bool Spells::canUsePower(const std::string &power) const @@ -232,26 +312,46 @@ namespace MWMechanics void Spells::readState(const ESM::SpellState &state) { - mSpells = state.mSpells; - mSelectedSpell = state.mSelectedSpell; - - // Discard spells that are no longer available due to changed content files - for (TContainer::iterator iter = mSpells.begin(); iter!=mSpells.end();) + for (TContainer::const_iterator it = state.mSpells.begin(); it != state.mSpells.end(); ++it) { - const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(iter->first); - if (!spell) + // Discard spells that are no longer available due to changed content files + const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(it->first); + if (spell) { - if (iter->first == mSelectedSpell) - mSelectedSpell = ""; - mSpells.erase(iter++); + mSpells[it->first] = it->second; + + if (it->first == state.mSelectedSpell) + mSelectedSpell = it->first; } - else - ++iter; } // No need to discard spells here (doesn't really matter if non existent ids are kept) for (std::map::const_iterator it = state.mUsedPowers.begin(); it != state.mUsedPowers.end(); ++it) mUsedPowers[it->first] = MWWorld::TimeStamp(it->second); + + for (std::map >::const_iterator it = + state.mPermanentSpellEffects.begin(); it != state.mPermanentSpellEffects.end(); ++it) + { + const ESM::Spell * spell = MWBase::Environment::get().getWorld()->getStore().get().search(it->first); + if (!spell) + continue; + + mPermanentSpellEffects[it->first] = MagicEffects(); + for (std::vector::const_iterator effectIt = it->second.begin(); effectIt != it->second.end(); ++effectIt) + { + mPermanentSpellEffects[it->first].add(EffectKey(effectIt->mId, effectIt->mArg), effectIt->mMagnitude); + } + } + + mCorprusSpells.clear(); + for (std::map::const_iterator it = state.mCorprusSpells.begin(); it != state.mCorprusSpells.end(); ++it) + { + if (mSpells.find(it->first) != mSpells.end()) // Discard unavailable corprus spells + { + mCorprusSpells[it->first].mWorsenings = state.mCorprusSpells.at(it->first).mWorsenings; + mCorprusSpells[it->first].mNextWorsening = MWWorld::TimeStamp(state.mCorprusSpells.at(it->first).mNextWorsening); + } + } } void Spells::writeState(ESM::SpellState &state) const @@ -261,5 +361,26 @@ namespace MWMechanics for (std::map::const_iterator it = mUsedPowers.begin(); it != mUsedPowers.end(); ++it) state.mUsedPowers[it->first] = it->second.toEsm(); + + for (std::map::const_iterator it = mPermanentSpellEffects.begin(); it != mPermanentSpellEffects.end(); ++it) + { + std::vector effectList; + for (MagicEffects::Collection::const_iterator effectIt = it->second.begin(); effectIt != it->second.end(); ++effectIt) + { + ESM::SpellState::PermanentSpellEffectInfo info; + info.mId = effectIt->first.mId; + info.mArg = effectIt->first.mArg; + info.mMagnitude = effectIt->second.getModifier(); + + effectList.push_back(info); + } + state.mPermanentSpellEffects[it->first] = effectList; + } + + for (std::map::const_iterator it = mCorprusSpells.begin(); it != mCorprusSpells.end(); ++it) + { + state.mCorprusSpells[it->first].mWorsenings = mCorprusSpells.at(it->first).mWorsenings; + state.mCorprusSpells[it->first].mNextWorsening = mCorprusSpells.at(it->first).mNextWorsening.toEsm(); + } } } diff --git a/apps/openmw/mwmechanics/spells.hpp b/apps/openmw/mwmechanics/spells.hpp index 6997a9d7a..064b2c1e5 100644 --- a/apps/openmw/mwmechanics/spells.hpp +++ b/apps/openmw/mwmechanics/spells.hpp @@ -31,21 +31,37 @@ namespace MWMechanics { public: - typedef std::map > TContainer; // ID, typedef TContainer::const_iterator TIterator; + struct CorprusStats + { + static const int sWorseningPeriod = 24; + + int mWorsenings; + MWWorld::TimeStamp mNextWorsening; + }; + private: TContainer mSpells; + // spell-tied effects that will be applied even after removing the spell (currently used to keep positive effects when corprus is removed) + std::map mPermanentSpellEffects; + // Note: this is the spell that's about to be cast, *not* the spell selected in the GUI (which may be different) std::string mSelectedSpell; std::map mUsedPowers; + std::map mCorprusSpells; + public: + void worsenCorprus(const std::string &corpSpellId); + static bool hasCorprusEffect(const ESM::Spell *spell); + const std::map & getCorprusSpells() const; + bool canUsePower (const std::string& power) const; void usePower (const std::string& power); @@ -58,11 +74,14 @@ namespace MWMechanics TIterator end() const; - bool hasSpell(const std::string& spell) { return mSpells.find(Misc::StringUtils::lowerCase(spell)) != mSpells.end(); } + bool hasSpell(const std::string& spell) const { return mSpells.find(Misc::StringUtils::lowerCase(spell)) != mSpells.end(); } void add (const std::string& spell); ///< Adding a spell that is already listed in *this is a no-op. + void add (const ESM::Spell* spell); + ///< Adding a spell that is already listed in *this is a no-op. + void remove (const std::string& spell); ///< If the spell to be removed is the selected spell, the selected spell will be changed to /// no spell (empty string). diff --git a/apps/openmw/mwmechanics/stat.cpp b/apps/openmw/mwmechanics/stat.cpp index 61b6d60ad..554f619a5 100644 --- a/apps/openmw/mwmechanics/stat.cpp +++ b/apps/openmw/mwmechanics/stat.cpp @@ -15,6 +15,11 @@ void MWMechanics::AttributeValue::readState (const ESM::StatState& state) mDamage = state.mDamage; } +void MWMechanics::AttributeValue::setModifiers(int fortify, int drain, int absorb) +{ + mFortified = fortify; + mModifier = (fortify - drain) - absorb; +} void MWMechanics::SkillValue::writeState (ESM::StatState& state) const { diff --git a/apps/openmw/mwmechanics/stat.hpp b/apps/openmw/mwmechanics/stat.hpp index 0fb4c5732..294dbcb6c 100644 --- a/apps/openmw/mwmechanics/stat.hpp +++ b/apps/openmw/mwmechanics/stat.hpp @@ -235,26 +235,29 @@ namespace MWMechanics class AttributeValue { int mBase; - int mModifier; - int mDamage; + int mFortified; + int mModifier; // net effect of Fortified, Drain & Absorb + float mDamage; // needs to be float to allow continuous damage public: - AttributeValue() : mBase(0), mModifier(0), mDamage(0) {} + AttributeValue() : mBase(0), mFortified(0), mModifier(0), mDamage(0) {} - int getModified() const { return std::max(0, mBase - mDamage + mModifier); } + int getModified() const { return std::max(0, mBase - (int) mDamage + mModifier); } int getBase() const { return mBase; } int getModifier() const { return mModifier; } void setBase(int base) { mBase = std::max(0, base); } - void setModifier(int mod) { mModifier = mod; } + void setModifiers(int fortify, int drain, int absorb); - void damage(int damage) { mDamage += damage; } - void restore(int amount) { mDamage -= std::min(mDamage, amount); } + void damage(float damage) { mDamage = std::min(mDamage + damage, (float)(mBase + mFortified)); } + void restore(float amount) { mDamage -= std::min(mDamage, amount); } int getDamage() const { return mDamage; } void writeState (ESM::StatState& state) const; void readState (const ESM::StatState& state); + + friend bool operator== (const AttributeValue& left, const AttributeValue& right); }; class SkillValue : public AttributeValue @@ -268,13 +271,16 @@ namespace MWMechanics void writeState (ESM::StatState& state) const; void readState (const ESM::StatState& state); + + friend bool operator== (const SkillValue& left, const SkillValue& right); }; inline bool operator== (const AttributeValue& left, const AttributeValue& right) { return left.getBase() == right.getBase() + && left.mFortified == right.mFortified && left.getModifier() == right.getModifier() - && left.getDamage() == right.getDamage(); + && left.mDamage == right.mDamage; } inline bool operator!= (const AttributeValue& left, const AttributeValue& right) { @@ -283,9 +289,8 @@ namespace MWMechanics inline bool operator== (const SkillValue& left, const SkillValue& right) { - return left.getBase() == right.getBase() - && left.getModifier() == right.getModifier() - && left.getDamage() == right.getDamage() + // delegate to base class for most of the work + return (static_cast(left) == right) && left.getProgress() == right.getProgress(); } inline bool operator!= (const SkillValue& left, const SkillValue& right) diff --git a/apps/openmw/mwmechanics/summoning.cpp b/apps/openmw/mwmechanics/summoning.cpp new file mode 100644 index 000000000..668031141 --- /dev/null +++ b/apps/openmw/mwmechanics/summoning.cpp @@ -0,0 +1,200 @@ +#include "summoning.hpp" + +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" + +#include "../mwmechanics/spellcasting.hpp" + +#include "../mwworld/esmstore.hpp" +#include "../mwworld/class.hpp" +#include "../mwworld/manualref.hpp" +#include "../mwworld/inventorystore.hpp" + +#include "../mwrender/animation.hpp" + +#include "creaturestats.hpp" +#include "aifollow.hpp" + + +namespace MWMechanics +{ + + void cleanupSummonedCreature (MWMechanics::CreatureStats& casterStats, int creatureActorId) + { + MWWorld::Ptr ptr = MWBase::Environment::get().getWorld()->searchPtrViaActorId(creatureActorId); + if (!ptr.isEmpty()) + { + // TODO: Show death animation before deleting? We shouldn't allow looting the corpse while the animation + // plays though, which is a rather lame exploit in vanilla. + MWBase::Environment::get().getWorld()->deleteObject(ptr); + + const ESM::Static* fx = MWBase::Environment::get().getWorld()->getStore().get() + .search("VFX_Summon_End"); + if (fx) + MWBase::Environment::get().getWorld()->spawnEffect("meshes\\" + fx->mModel, + "", Ogre::Vector3(ptr.getRefData().getPosition().pos)); + } + else + { + // We didn't find the creature. It's probably in an inactive cell. + // Add to graveyard so we can delete it when the cell becomes active. + std::vector& graveyard = casterStats.getSummonedCreatureGraveyard(); + graveyard.push_back(creatureActorId); + } + } + + UpdateSummonedCreatures::UpdateSummonedCreatures(const MWWorld::Ptr &actor) + : mActor(actor) + { + + } + + UpdateSummonedCreatures::~UpdateSummonedCreatures() + { + } + + void UpdateSummonedCreatures::visit(EffectKey key, const std::string &sourceName, const std::string &sourceId, int casterActorId, float magnitude, float remainingTime, float totalTime) + { + if (isSummoningEffect(key.mId) && magnitude > 0) + { + mActiveEffects.insert(std::make_pair(key.mId, sourceId)); + } + } + + void UpdateSummonedCreatures::finish() + { + static std::map summonMap; + if (summonMap.empty()) + { + summonMap[ESM::MagicEffect::SummonAncestralGhost] = "sMagicAncestralGhostID"; + summonMap[ESM::MagicEffect::SummonBonelord] = "sMagicBonelordID"; + summonMap[ESM::MagicEffect::SummonBonewalker] = "sMagicLeastBonewalkerID"; + summonMap[ESM::MagicEffect::SummonCenturionSphere] = "sMagicCenturionSphereID"; + summonMap[ESM::MagicEffect::SummonClannfear] = "sMagicClannfearID"; + summonMap[ESM::MagicEffect::SummonDaedroth] = "sMagicDaedrothID"; + summonMap[ESM::MagicEffect::SummonDremora] = "sMagicDremoraID"; + summonMap[ESM::MagicEffect::SummonFabricant] = "sMagicFabricantID"; + summonMap[ESM::MagicEffect::SummonFlameAtronach] = "sMagicFlameAtronachID"; + summonMap[ESM::MagicEffect::SummonFrostAtronach] = "sMagicFrostAtronachID"; + summonMap[ESM::MagicEffect::SummonGoldenSaint] = "sMagicGoldenSaintID"; + summonMap[ESM::MagicEffect::SummonGreaterBonewalker] = "sMagicGreaterBonewalkerID"; + summonMap[ESM::MagicEffect::SummonHunger] = "sMagicHungerID"; + summonMap[ESM::MagicEffect::SummonScamp] = "sMagicScampID"; + summonMap[ESM::MagicEffect::SummonSkeletalMinion] = "sMagicSkeletalMinionID"; + summonMap[ESM::MagicEffect::SummonStormAtronach] = "sMagicStormAtronachID"; + summonMap[ESM::MagicEffect::SummonWingedTwilight] = "sMagicWingedTwilightID"; + summonMap[ESM::MagicEffect::SummonWolf] = "sMagicCreature01ID"; + summonMap[ESM::MagicEffect::SummonBear] = "sMagicCreature02ID"; + summonMap[ESM::MagicEffect::SummonBonewolf] = "sMagicCreature03ID"; + summonMap[ESM::MagicEffect::SummonCreature04] = "sMagicCreature04ID"; + summonMap[ESM::MagicEffect::SummonCreature05] = "sMagicCreature05ID"; + } + + MWMechanics::CreatureStats& creatureStats = mActor.getClass().getCreatureStats(mActor); + + // Update summon effects + std::map& creatureMap = creatureStats.getSummonedCreatureMap(); + for (std::map::iterator it = creatureMap.begin(); it != creatureMap.end(); ) + { + bool found = mActiveEffects.find(it->first) != mActiveEffects.end(); + if (!found) + { + // Effect has ended + cleanupSummonedCreature(creatureStats, it->second); + creatureMap.erase(it++); + continue; + } + ++it; + } + + for (std::set >::iterator it = mActiveEffects.begin(); it != mActiveEffects.end(); ++it) + { + bool found = creatureMap.find(std::make_pair(it->first, it->second)) != creatureMap.end(); + if (!found) + { + ESM::Position ipos = mActor.getRefData().getPosition(); + Ogre::Vector3 pos(ipos.pos); + Ogre::Quaternion rot(Ogre::Radian(-ipos.rot[2]), Ogre::Vector3::UNIT_Z); + const float distance = 50; + pos = pos + distance*rot.yAxis(); + ipos.pos[0] = pos.x; + ipos.pos[1] = pos.y; + ipos.pos[2] = pos.z; + ipos.rot[0] = 0; + ipos.rot[1] = 0; + ipos.rot[2] = 0; + + const std::string& creatureGmst = summonMap[it->first]; + std::string creatureID = + MWBase::Environment::get().getWorld()->getStore().get().find(creatureGmst)->getString(); + + if (!creatureID.empty()) + { + MWWorld::CellStore* store = mActor.getCell(); + MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), creatureID, 1); + ref.getPtr().getCellRef().setPosition(ipos); + + MWMechanics::CreatureStats& summonedCreatureStats = ref.getPtr().getClass().getCreatureStats(ref.getPtr()); + + // Make the summoned creature follow its master and help in fights + AiFollow package(mActor.getCellRef().getRefId()); + summonedCreatureStats.getAiSequence().stack(package, ref.getPtr()); + int creatureActorId = summonedCreatureStats.getActorId(); + + MWWorld::Ptr placed = MWBase::Environment::get().getWorld()->safePlaceObject(ref.getPtr(),store,ipos); + + MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(placed); + if (anim) + { + const ESM::Static* fx = MWBase::Environment::get().getWorld()->getStore().get() + .search("VFX_Summon_Start"); + if (fx) + anim->addEffect("meshes\\" + fx->mModel, -1, false); + } + + creatureMap.insert(std::make_pair(*it, creatureActorId)); + } + } + } + + for (std::map::iterator it = creatureMap.begin(); it != creatureMap.end(); ) + { + MWWorld::Ptr ptr = MWBase::Environment::get().getWorld()->searchPtrViaActorId(it->second); + if (!ptr.isEmpty() && ptr.getClass().getCreatureStats(ptr).isDead()) + { + // Purge the magic effect so a new creature can be summoned if desired + creatureStats.getActiveSpells().purgeEffect(it->first.first, it->first.second); + if (mActor.getClass().hasInventoryStore(ptr)) + mActor.getClass().getInventoryStore(mActor).purgeEffect(it->first.first, it->first.second); + + cleanupSummonedCreature(creatureStats, it->second); + creatureMap.erase(it++); + } + else + ++it; + } + + std::vector& graveyard = creatureStats.getSummonedCreatureGraveyard(); + for (std::vector::iterator it = graveyard.begin(); it != graveyard.end(); ) + { + MWWorld::Ptr ptr = MWBase::Environment::get().getWorld()->searchPtrViaActorId(*it); + if (!ptr.isEmpty()) + { + it = graveyard.erase(it); + + const ESM::Static* fx = MWBase::Environment::get().getWorld()->getStore().get() + .search("VFX_Summon_End"); + if (fx) + MWBase::Environment::get().getWorld()->spawnEffect("meshes\\" + fx->mModel, + "", Ogre::Vector3(ptr.getRefData().getPosition().pos)); + + MWBase::Environment::get().getWorld()->deleteObject(ptr); + } + else + ++it; + } + } + +} diff --git a/apps/openmw/mwmechanics/summoning.hpp b/apps/openmw/mwmechanics/summoning.hpp new file mode 100644 index 000000000..8e418cdeb --- /dev/null +++ b/apps/openmw/mwmechanics/summoning.hpp @@ -0,0 +1,36 @@ +#ifndef OPENMW_MECHANICS_SUMMONING_H +#define OPENMW_MECHANICS_SUMMONING_H + +#include + +#include "magiceffects.hpp" +#include "../mwworld/ptr.hpp" + +namespace MWMechanics +{ + + class CreatureStats; + + struct UpdateSummonedCreatures : public EffectSourceVisitor + { + UpdateSummonedCreatures(const MWWorld::Ptr& actor); + virtual ~UpdateSummonedCreatures(); + + virtual void visit (MWMechanics::EffectKey key, + const std::string& sourceName, const std::string& sourceId, int casterActorId, + float magnitude, float remainingTime = -1, float totalTime = -1); + + /// To call after all effect sources have been visited + void finish(); + + private: + MWWorld::Ptr mActor; + + std::set > mActiveEffects; + }; + + void cleanupSummonedCreature (MWMechanics::CreatureStats& casterStats, int creatureActorId); + +} + +#endif diff --git a/apps/openmw/mwrender/activatoranimation.cpp b/apps/openmw/mwrender/activatoranimation.cpp index de0457e57..1ef68f619 100644 --- a/apps/openmw/mwrender/activatoranimation.cpp +++ b/apps/openmw/mwrender/activatoranimation.cpp @@ -1,8 +1,9 @@ #include "activatoranimation.hpp" -#include +#include +#include -#include "../mwbase/world.hpp" +#include #include "renderconst.hpp" @@ -13,20 +14,37 @@ ActivatorAnimation::~ActivatorAnimation() { } -ActivatorAnimation::ActivatorAnimation(const MWWorld::Ptr &ptr) +ActivatorAnimation::ActivatorAnimation(const MWWorld::Ptr &ptr, const std::string& model) : Animation(ptr, ptr.getRefData().getBaseNode()) { - MWWorld::LiveCellRef *ref = mPtr.get(); + if(!model.empty()) + { + setObjectRoot(model, false); + setRenderProperties(mObjectRoot, RV_Misc, RQG_Main, RQG_Alpha); - assert(ref->mBase != NULL); - if(!ref->mBase->mModel.empty()) + addAnimSource(model); + } + else { - const std::string name = "meshes\\"+ref->mBase->mModel; + // No model given. Create an object root anyway, so that lights can be added to it if needed. + mObjectRoot = NifOgre::ObjectScenePtr (new NifOgre::ObjectScene(mInsert->getCreator())); + } +} - setObjectRoot(name, false); - setRenderProperties(mObjectRoot, RV_Misc, RQG_Main, RQG_Alpha); +void ActivatorAnimation::addLight(const ESM::Light *light) +{ + addExtraLight(mInsert->getCreator(), mObjectRoot, light); +} - addAnimSource(name); +void ActivatorAnimation::removeParticles() +{ + for (unsigned int i=0; imParticles.size(); ++i) + { + // Don't destroyParticleSystem, the ParticleSystemController is still holding a pointer to it. + // Don't setVisible, this could conflict with a VisController. + // The following will remove all spawned particles, then set the speed factor to zero so that no new ones will be spawned. + mObjectRoot->mParticles[i]->setSpeedFactor(0.f); + mObjectRoot->mParticles[i]->clear(); } } diff --git a/apps/openmw/mwrender/activatoranimation.hpp b/apps/openmw/mwrender/activatoranimation.hpp index eb3e5815e..a234defe7 100644 --- a/apps/openmw/mwrender/activatoranimation.hpp +++ b/apps/openmw/mwrender/activatoranimation.hpp @@ -13,8 +13,11 @@ namespace MWRender class ActivatorAnimation : public Animation { public: - ActivatorAnimation(const MWWorld::Ptr& ptr); + ActivatorAnimation(const MWWorld::Ptr& ptr, const std::string &model); virtual ~ActivatorAnimation(); + + void addLight(const ESM::Light *light); + void removeParticles(); }; } diff --git a/apps/openmw/mwrender/actors.cpp b/apps/openmw/mwrender/actors.cpp index b7e9f5730..3da6c40c8 100644 --- a/apps/openmw/mwrender/actors.cpp +++ b/apps/openmw/mwrender/actors.cpp @@ -71,22 +71,31 @@ void Actors::insertNPC(const MWWorld::Ptr& ptr) mAllActors[ptr] = anim; mRendering->addWaterRippleEmitter (ptr); } -void Actors::insertCreature (const MWWorld::Ptr& ptr, bool weaponsShields) +void Actors::insertCreature (const MWWorld::Ptr& ptr, const std::string &model, bool weaponsShields) { insertBegin(ptr); Animation* anim = NULL; if (weaponsShields) - anim = new CreatureWeaponAnimation(ptr); + anim = new CreatureWeaponAnimation(ptr, model); else - anim = new CreatureAnimation(ptr); + anim = new CreatureAnimation(ptr, model); delete mAllActors[ptr]; mAllActors[ptr] = anim; mRendering->addWaterRippleEmitter (ptr); } -void Actors::insertActivator (const MWWorld::Ptr& ptr) +void Actors::insertActivator (const MWWorld::Ptr& ptr, const std::string &model, bool addLight) { insertBegin(ptr); - ActivatorAnimation* anim = new ActivatorAnimation(ptr); + ActivatorAnimation* anim = new ActivatorAnimation(ptr, model); + + if(ptr.getTypeName() == typeid(ESM::Light).name()) + { + if (addLight) + anim->addLight(ptr.get()->mBase); + else + anim->removeParticles(); + } + delete mAllActors[ptr]; mAllActors[ptr] = anim; } @@ -189,4 +198,18 @@ void Actors::updateObjectCell(const MWWorld::Ptr &old, const MWWorld::Ptr &cur) mRendering->updateWaterRippleEmitterPtr (old, cur); } +void Actors::enableLights() +{ + PtrAnimationMap::const_iterator it = mAllActors.begin(); + for(;it != mAllActors.end();++it) + it->second->enableLights(true); +} + +void Actors::disableLights() +{ + PtrAnimationMap::const_iterator it = mAllActors.begin(); + for(;it != mAllActors.end();++it) + it->second->enableLights(false); +} + } diff --git a/apps/openmw/mwrender/actors.hpp b/apps/openmw/mwrender/actors.hpp index d5d6c52bb..4f6c1bec2 100644 --- a/apps/openmw/mwrender/actors.hpp +++ b/apps/openmw/mwrender/actors.hpp @@ -40,11 +40,14 @@ namespace MWRender void setRootNode(Ogre::SceneNode* root); void insertNPC(const MWWorld::Ptr& ptr); - void insertCreature (const MWWorld::Ptr& ptr, bool weaponsShields); - void insertActivator (const MWWorld::Ptr& ptr); - bool deleteObject (const MWWorld::Ptr& ptr); + void insertCreature (const MWWorld::Ptr& ptr, const std::string& model, bool weaponsShields); + void insertActivator (const MWWorld::Ptr& ptr, const std::string& model, bool addLight=false); + bool deleteObject (const MWWorld::Ptr& ptr); ///< \return found? + void enableLights(); + void disableLights(); + void removeCell(MWWorld::CellStore* store); void update (Ogre::Camera* camera); diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index 872740d74..01a88faf2 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -17,6 +17,8 @@ #include #include #include +#include +#include #include @@ -70,6 +72,7 @@ Animation::Animation(const MWWorld::Ptr &ptr, Ogre::SceneNode *node) , mNonAccumCtrl(NULL) , mAccumulate(0.0f) , mNullAnimationTimePtr(OGRE_NEW NullAnimationTime) + , mGlowLight(NULL) { for(size_t i = 0;i < sNumGroups;i++) mAnimationTimePtr[i].bind(OGRE_NEW AnimationTime(this)); @@ -77,11 +80,19 @@ Animation::Animation(const MWWorld::Ptr &ptr, Ogre::SceneNode *node) Animation::~Animation() { + setLightEffect(0); + mEffects.clear(); mAnimSources.clear(); } +std::string Animation::getObjectRootName() const +{ + if (mSkelBase) + return mSkelBase->getMesh()->getName(); + return std::string(); +} void Animation::setObjectRoot(const std::string &model, bool baseonly) { @@ -93,22 +104,9 @@ void Animation::setObjectRoot(const std::string &model, bool baseonly) if(model.empty()) return; - std::string mdlname = Misc::StringUtils::lowerCase(model); - std::string::size_type p = mdlname.rfind('\\'); - if(p == std::string::npos) - p = mdlname.rfind('/'); - if(p != std::string::npos) - mdlname.insert(mdlname.begin()+p+1, 'x'); - else - mdlname.insert(mdlname.begin(), 'x'); - if(!Ogre::ResourceGroupManager::getSingleton().resourceExistsInAnyGroup(mdlname)) - { - mdlname = model; - Misc::StringUtils::toLower(mdlname); - } + mObjectRoot = (!baseonly ? NifOgre::Loader::createObjects(mInsert, model) : + NifOgre::Loader::createObjectBase(mInsert, model)); - mObjectRoot = (!baseonly ? NifOgre::Loader::createObjects(mInsert, mdlname) : - NifOgre::Loader::createObjectBase(mInsert, mdlname)); if(mObjectRoot->mSkelBase) { mSkelBase = mObjectRoot->mSkelBase; @@ -164,6 +162,9 @@ struct AddGlow instance->setProperty("env_map", sh::makeProperty(new sh::BooleanValue(true))); instance->setProperty("env_map_color", sh::makeProperty(new sh::Vector3(mColor->x, mColor->y, mColor->z))); + // Workaround for crash in Ogre (https://bitbucket.org/sinbad/ogre/pull-request/447/fix-shadows-crash-for-textureunitstates/diff) + // Remove when the fix is merged + instance->getMaterial()->setShadowCasterMaterial("openmw_shadowcaster_noalpha"); } }; @@ -247,14 +248,8 @@ void Animation::addAnimSource(const std::string &model) if(!mSkelBase) return; - std::string kfname = Misc::StringUtils::lowerCase(model); - std::string::size_type p = kfname.rfind('\\'); - if(p == std::string::npos) - p = kfname.rfind('/'); - if(p != std::string::npos) - kfname.insert(kfname.begin()+p+1, 'x'); - else - kfname.insert(kfname.begin(), 'x'); + std::string kfname = model; + Misc::StringUtils::toLower(kfname); if(kfname.size() > 4 && kfname.compare(kfname.size()-4, 4, ".nif") == 0) kfname.replace(kfname.size()-4, 4, ".kf"); @@ -289,7 +284,7 @@ void Animation::addAnimSource(const std::string &model) } } - if (grp == 0 && dstval->getNode()->getName() == "Bip01") + if (grp == 0 && (dstval->getNode()->getName() == "Bip01" || dstval->getNode()->getName() == "Root Bone")) { mNonAccumRoot = dstval->getNode(); mAccumRoot = mNonAccumRoot->getParent(); @@ -331,7 +326,7 @@ void Animation::addExtraLight(Ogre::SceneManager *sceneMgr, NifOgre::ObjectScene { const MWWorld::Fallback *fallback = MWBase::Environment::get().getWorld()->getFallback(); - const int clr = light->mData.mColor; + const unsigned int clr = light->mData.mColor; Ogre::ColourValue color(((clr >> 0) & 0xFF) / 255.0f, ((clr >> 8) & 0xFF) / 255.0f, ((clr >> 16) & 0xFF) / 255.0f); @@ -409,7 +404,7 @@ void Animation::addExtraLight(Ogre::SceneManager *sceneMgr, NifOgre::ObjectScene } -Ogre::Node *Animation::getNode(const std::string &name) +Ogre::Node* Animation::getNode(const std::string &name) { if(mSkelBase) { @@ -420,6 +415,16 @@ Ogre::Node *Animation::getNode(const std::string &name) return NULL; } +Ogre::Node* Animation::getNode(int handle) +{ + if (mSkelBase) + { + Ogre::SkeletonInstance *skel = mSkelBase->getSkeleton(); + return skel->getBone(handle); + } + return NULL; +} + NifOgre::TextKeyMap::const_iterator Animation::findGroupStart(const NifOgre::TextKeyMap &keys, const std::string &groupname) { NifOgre::TextKeyMap::const_iterator iter(keys.begin()); @@ -467,7 +472,13 @@ float Animation::calcAnimVelocity(const NifOgre::TextKeyMap &keys, NifOgre::Node const std::string stop = groupname+": stop"; float starttime = std::numeric_limits::max(); float stoptime = 0.0f; - // Have to find keys in reverse (see reset method) + + // Pick the last Loop Stop key and the last Loop Start key. + // This is required because of broken text keys in AshVampire.nif. + // It has *two* WalkForward: Loop Stop keys at different times, the first one is used for stopping playback + // but the animation velocity calculation uses the second one. + // As result the animation velocity calculation is not correct, and this incorrect velocity must be replicated, + // because otherwise the Creature's Speed (dagoth uthol) would not be sufficient to move fast enough. NifOgre::TextKeyMap::const_reverse_iterator keyiter(keys.rbegin()); while(keyiter != keys.rend()) { @@ -476,8 +487,18 @@ float Animation::calcAnimVelocity(const NifOgre::TextKeyMap &keys, NifOgre::Node starttime = keyiter->first; break; } - else if(keyiter->second == loopstop || keyiter->second == stop) + ++keyiter; + } + keyiter = keys.rbegin(); + while(keyiter != keys.rend()) + { + if (keyiter->second == stop) + stoptime = keyiter->first; + else if (keyiter->second == loopstop) + { stoptime = keyiter->first; + break; + } ++keyiter; } @@ -496,7 +517,7 @@ float Animation::getVelocity(const std::string &groupname) const { /* Look in reverse; last-inserted source has priority. */ AnimSourceList::const_reverse_iterator animsrc(mAnimSources.rbegin()); - for(;animsrc != mAnimSources.rend();animsrc++) + for(;animsrc != mAnimSources.rend();++animsrc) { const NifOgre::TextKeyMap &keys = (*animsrc)->mTextKeys; if(findGroupStart(keys, groupname) != keys.end()) @@ -549,7 +570,8 @@ float Animation::getVelocity(const std::string &groupname) const static void updateBoneTree(const Ogre::SkeletonInstance *skelsrc, Ogre::Bone *bone) { - if(skelsrc->hasBone(bone->getName())) + if(bone->getName() != " " // really should be != "", but see workaround in skeleton.cpp for empty node names + && skelsrc->hasBone(bone->getName())) { Ogre::Bone *srcbone = skelsrc->getBone(bone->getName()); if(!srcbone->getParent() || !bone->getParent()) @@ -591,7 +613,7 @@ void Animation::updatePosition(float oldtime, float newtime, Ogre::Vector3 &posi mAccumRoot->setPosition(-off); } -bool Animation::reset(AnimState &state, const NifOgre::TextKeyMap &keys, const std::string &groupname, const std::string &start, const std::string &stop, float startpoint) +bool Animation::reset(AnimState &state, const NifOgre::TextKeyMap &keys, const std::string &groupname, const std::string &start, const std::string &stop, float startpoint, bool loopfallback) { // Look for text keys in reverse. This normally wouldn't matter, but for some reason undeadwolf_2.nif has two // separate walkforward keys, and the last one is supposed to be used. @@ -632,8 +654,16 @@ bool Animation::reset(AnimState &state, const NifOgre::TextKeyMap &keys, const s return false; state.mStartTime = startkey->first; - state.mLoopStartTime = startkey->first; - state.mLoopStopTime = stopkey->first; + if (loopfallback) + { + state.mLoopStartTime = startkey->first; + state.mLoopStopTime = stopkey->first; + } + else + { + state.mLoopStartTime = startkey->first; + state.mLoopStopTime = std::numeric_limits::max(); + } state.mStopTime = stopkey->first; state.mTime = state.mStartTime + ((state.mStopTime - state.mStartTime) * startpoint); @@ -703,7 +733,7 @@ void Animation::handleTextKey(AnimState &state, const std::string &groupname, co { MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); MWBase::SoundManager::PlayType type = MWBase::SoundManager::Play_TypeSfx; - if(evt.compare(10, evt.size()-10, "left") == 0 || evt.compare(10, evt.size()-10, "right") == 0) + if(evt.compare(10, evt.size()-10, "left") == 0 || evt.compare(10, evt.size()-10, "right") == 0 || evt.compare(10, evt.size()-10, "land") == 0) type = MWBase::SoundManager::Play_TypeFoot; sndMgr->playSound3D(mPtr, sound, volume, pitch, type); } @@ -780,7 +810,25 @@ void Animation::handleTextKey(AnimState &state, const std::string &groupname, co attachArrow(); else if (groupname == "spellcast" && evt.substr(evt.size()-7, 7) == "release") - MWBase::Environment::get().getWorld()->castSpell(mPtr); + { + // Make sure this key is actually for the RangeType we are casting. The flame atronach has + // the same animation for all range types, so there are 3 "release" keys on the same time, one for each range type. + // FIXME: This logic should really be in the CharacterController + const std::string& spellid = mPtr.getClass().getCreatureStats(mPtr).getSpells().getSelectedSpell(); + const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().find(spellid); + const ESM::ENAMstruct &effectentry = spell->mEffects.mList.at(0); + int range = 0; + if (evt.compare(off, len, "self release") == 0) + range = 0; + else if (evt.compare(off, len, "touch release") == 0) + range = 1; + else if (evt.compare(off, len, "target release") == 0) + range = 2; + if (effectentry.mRange == range) + { + MWBase::Environment::get().getWorld()->castSpell(mPtr); + } + } else if (groupname == "shield" && evt.compare(off, len, "block hit") == 0) mPtr.getClass().block(mPtr); @@ -788,8 +836,7 @@ void Animation::handleTextKey(AnimState &state, const std::string &groupname, co void Animation::changeGroups(const std::string &groupname, int groups) { - AnimStateMap::iterator stateiter = mStates.begin(); - stateiter = mStates.find(groupname); + AnimStateMap::iterator stateiter = mStates.find(groupname); if(stateiter != mStates.end()) { if(stateiter->second.mGroups != groups) @@ -800,7 +847,18 @@ void Animation::changeGroups(const std::string &groupname, int groups) return; } } -void Animation::play(const std::string &groupname, int priority, int groups, bool autodisable, float speedmult, const std::string &start, const std::string &stop, float startpoint, size_t loops) + +void Animation::stopLooping(const std::string& groupname) +{ + AnimStateMap::iterator stateiter = mStates.find(groupname); + if(stateiter != mStates.end()) + { + stateiter->second.mLoopCount = 0; + return; + } +} + +void Animation::play(const std::string &groupname, int priority, int groups, bool autodisable, float speedmult, const std::string &start, const std::string &stop, float startpoint, size_t loops, bool loopfallback) { if(!mSkelBase || mAnimSources.empty()) return; @@ -836,7 +894,7 @@ void Animation::play(const std::string &groupname, int priority, int groups, boo for(;iter != mAnimSources.rend();++iter) { const NifOgre::TextKeyMap &textkeys = (*iter)->mTextKeys; - if(reset(state, textkeys, groupname, start, stop, startpoint)) + if(reset(state, textkeys, groupname, start, stop, startpoint, loopfallback)) { state.mSource = *iter; state.mSpeedMult = speedmult; @@ -848,10 +906,13 @@ void Animation::play(const std::string &groupname, int priority, int groups, boo mStates[groupname] = state; NifOgre::TextKeyMap::const_iterator textkey(textkeys.lower_bound(state.mTime)); - while(textkey != textkeys.end() && textkey->first <= state.mTime) + if (state.mPlaying) { - handleTextKey(state, groupname, textkey, textkeys); - ++textkey; + while(textkey != textkeys.end() && textkey->first <= state.mTime) + { + handleTextKey(state, groupname, textkey, textkeys); + ++textkey; + } } if(state.mTime >= state.mLoopStopTime && state.mLoopCount > 0) @@ -862,7 +923,7 @@ void Animation::play(const std::string &groupname, int priority, int groups, boo if(state.mTime >= state.mLoopStopTime) break; - textkey = textkeys.lower_bound(state.mTime); + NifOgre::TextKeyMap::const_iterator textkey(textkeys.lower_bound(state.mTime)); while(textkey != textkeys.end() && textkey->first <= state.mTime) { handleTextKey(state, groupname, textkey, textkeys); @@ -886,6 +947,13 @@ void Animation::play(const std::string &groupname, int priority, int groups, boo } } +void Animation::adjustSpeedMult(const std::string &groupname, float speedmult) +{ + AnimStateMap::iterator state(mStates.find(groupname)); + if(state != mStates.end()) + state->second.mSpeedMult = speedmult; +} + bool Animation::isPlaying(const std::string &groupname) const { AnimStateMap::const_iterator state(mStates.find(groupname)); @@ -920,7 +988,11 @@ void Animation::resetActiveGroups() AnimStateMap::const_iterator state = mStates.find(mAnimationTimePtr[0]->getAnimName()); if(state == mStates.end()) + { + if (mAccumRoot && mNonAccumRoot) + mAccumRoot->setPosition(-mNonAccumRoot->getPosition()*mAccumulate); return; + } const Ogre::SharedPtr &animsrc = state->second.mSource; const std::vector >&ctrls = animsrc->mControllers[0]; @@ -934,6 +1006,9 @@ void Animation::resetActiveGroups() break; } } + + if (mAccumRoot && mNonAccumCtrl) + mAccumRoot->setPosition(-mNonAccumCtrl->getTranslation(state->second.mTime)*mAccumulate); } @@ -1071,9 +1146,6 @@ Ogre::Vector3 Animation::runAnimation(float duration) if(!state.mPlaying && state.mAutoDisable) { - if(mNonAccumCtrl && stateiter->first == mAnimationTimePtr[0]->getAnimName()) - mAccumRoot->setPosition(0.f,0.f,0.f); - mStates.erase(stateiter++); resetActiveGroups(); @@ -1173,12 +1245,13 @@ void Animation::detachObjectFromBone(Ogre::MovableObject *obj) mSkelBase->detachObjectFromBone(obj); } -bool Animation::allowSwitchViewMode() const +bool Animation::upperBodyReady() const { for (AnimStateMap::const_iterator stateiter = mStates.begin(); stateiter != mStates.end(); ++stateiter) { - if(stateiter->second.mPriority > MWMechanics::Priority_Movement + if((stateiter->second.mPriority > MWMechanics::Priority_Movement && stateiter->second.mPriority < MWMechanics::Priority_Torch) + || stateiter->second.mPriority == MWMechanics::Priority_Death) return false; } return true; @@ -1191,23 +1264,20 @@ void Animation::addEffect(const std::string &model, int effectId, bool loop, con if (it->mLoop && loop && it->mEffectId == effectId && it->mBoneName == bonename) return; - // fix texture extension to .dds - if (texture.size() > 4) - { - texture[texture.size()-3] = 'd'; - texture[texture.size()-2] = 'd'; - texture[texture.size()-1] = 's'; - } + std::string correctedTexture = Misc::ResourceHelpers::correctTexturePath(texture); EffectParams params; params.mModelName = model; if (bonename.empty()) params.mObjects = NifOgre::Loader::createObjects(mInsert, model); else - params.mObjects = NifOgre::Loader::createObjects(mSkelBase, bonename, mInsert, model); + { + if (!mSkelBase) + return; + params.mObjects = NifOgre::Loader::createObjects(mSkelBase, bonename, "", mInsert, model); + } - // TODO: turn off shadow casting - setRenderProperties(params.mObjects, RV_Misc, + setRenderProperties(params.mObjects, RV_Effects, RQG_Main, RQG_Alpha, 0.f, false, NULL); params.mLoop = loop; @@ -1255,7 +1325,7 @@ void Animation::addEffect(const std::string &model, int effectId, bool loop, con for (int tex=0; texgetNumTextureUnitStates(); ++tex) { Ogre::TextureUnitState* tus = pass->getTextureUnitState(tex); - tus->setTextureName("textures\\" + texture); + tus->setTextureName(correctedTexture); } } } @@ -1285,7 +1355,7 @@ void Animation::addEffect(const std::string &model, int effectId, bool loop, con for (int tex=0; texgetNumTextureUnitStates(); ++tex) { Ogre::TextureUnitState* tus = pass->getTextureUnitState(tex); - tus->setTextureName("textures\\" + texture); + tus->setTextureName(correctedTexture); } } } @@ -1380,6 +1450,37 @@ Ogre::Vector3 Animation::getEnchantmentColor(MWWorld::Ptr item) return result; } +void Animation::setLightEffect(float effect) +{ + if (effect == 0) + { + if (mGlowLight) + { + mInsert->getCreator()->destroySceneNode(mGlowLight->getParentSceneNode()); + mInsert->getCreator()->destroyLight(mGlowLight); + mGlowLight = NULL; + } + } + else + { + if (!mGlowLight) + { + mGlowLight = mInsert->getCreator()->createLight(); + + Ogre::AxisAlignedBox bounds = Ogre::AxisAlignedBox::BOX_NULL; + for(size_t i = 0;i < mObjectRoot->mEntities.size();i++) + { + Ogre::Entity *ent = mObjectRoot->mEntities[i]; + bounds.merge(ent->getBoundingBox()); + } + mInsert->createChildSceneNode(bounds.getCenter())->attachObject(mGlowLight); + } + mGlowLight->setType(Ogre::Light::LT_POINT); + effect += 3; + mGlowLight->setAttenuation(1.0f / (0.03 * (0.5/effect)), 0, 0.5/effect, 0); + } +} + ObjectAnimation::ObjectAnimation(const MWWorld::Ptr& ptr, const std::string &model) : Animation(ptr, ptr.getRefData().getBaseNode()) @@ -1410,11 +1511,6 @@ ObjectAnimation::ObjectAnimation(const MWWorld::Ptr& ptr, const std::string &mod } } -void ObjectAnimation::addLight(const ESM::Light *light) -{ - addExtraLight(mInsert->getCreator(), mObjectRoot, light); -} - class FindEntityTransparency { public: diff --git a/apps/openmw/mwrender/animation.hpp b/apps/openmw/mwrender/animation.hpp index e15bd6ecb..dab8cfebb 100644 --- a/apps/openmw/mwrender/animation.hpp +++ b/apps/openmw/mwrender/animation.hpp @@ -126,6 +126,8 @@ protected: MWWorld::Ptr mPtr; + Ogre::Light* mGlowLight; + Ogre::SceneNode *mInsert; Ogre::Entity *mSkelBase; NifOgre::ObjectScenePtr mObjectRoot; @@ -171,7 +173,7 @@ protected: */ bool reset(AnimState &state, const NifOgre::TextKeyMap &keys, const std::string &groupname, const std::string &start, const std::string &stop, - float startpoint); + float startpoint, bool loopfallback); void handleTextKey(AnimState &state, const std::string &groupname, const NifOgre::TextKeyMap::const_iterator &key, const NifOgre::TextKeyMap& map); @@ -204,6 +206,9 @@ public: Ogre::uint8 transqueue, Ogre::Real dist=0.0f, bool enchantedGlow=false, Ogre::Vector3* glowColor=NULL); + /// Returns the name of the .nif file that makes up this animation's base skeleton. + /// If there is no skeleton, returns "". + std::string getObjectRootName() const; Animation(const MWWorld::Ptr &ptr, Ogre::SceneNode *node); virtual ~Animation(); @@ -226,6 +231,7 @@ public: virtual void preRender (Ogre::Camera* camera); virtual void setAlpha(float alpha) {} + virtual void setVampire(bool vampire) {} public: void updatePtr(const MWWorld::Ptr &ptr); @@ -252,17 +258,28 @@ public: * at the start marker, 1 starts at the stop marker. * \param loops How many times to loop the animation. This will use the * "loop start" and "loop stop" markers if they exist, - * otherwise it will use "start" and "stop". + * otherwise it may fall back to "start" and "stop", but only if + * the \a loopFallback parameter is true. + * \param loopFallback Allow looping an animation that has no loop keys, i.e. fall back to use + * the "start" and "stop" keys for looping? */ void play(const std::string &groupname, int priority, int groups, bool autodisable, float speedmult, const std::string &start, const std::string &stop, - float startpoint, size_t loops); + float startpoint, size_t loops, bool loopfallback=false); + + /** If the given animation group is currently playing, set its remaining loop count to '0'. + */ + void stopLooping(const std::string& groupName); + + /** Adjust the speed multiplier of an already playing animation. + */ + void adjustSpeedMult (const std::string& groupname, float speedmult); /** Returns true if the named animation group is playing. */ bool isPlaying(const std::string &groupname) const; - //Checks if playing any animation which shouldn't be stopped when switching camera view modes - bool allowSwitchViewMode() const; + /// Returns true if no important animations are currently playing on the upper body. + bool upperBodyReady() const; /** Gets info about the given animation group. * \param groupname Animation group to check. @@ -295,21 +312,32 @@ public: /// A relative factor (0-1) that decides if and how much the skeleton should be pitched /// to indicate the facing orientation of the character. virtual void setPitchFactor(float factor) {} + virtual void setHeadPitch(Ogre::Radian factor) {} + virtual void setHeadYaw(Ogre::Radian factor) {} + virtual Ogre::Radian getHeadPitch() const { return Ogre::Radian(0.f); } + virtual Ogre::Radian getHeadYaw() const { return Ogre::Radian(0.f); } virtual Ogre::Vector3 runAnimation(float duration); /// This is typically called as part of runAnimation, but may be called manually if needed. void updateEffects(float duration); + // TODO: move outside of this class + /// Makes this object glow, by placing a Light in its center. + /// @param effect Controls the radius and intensity of the light. + void setLightEffect(float effect); + virtual void showWeapons(bool showWeapon); virtual void showCarriedLeft(bool show) {} virtual void attachArrow() {} virtual void releaseArrow() {} void enableLights(bool enable); + virtual void enableHeadAnimation(bool enable) {} Ogre::AxisAlignedBox getWorldBounds(); Ogre::Node *getNode(const std::string &name); + Ogre::Node *getNode(int handle); // Attaches the given object to a bone on this object's base skeleton. If the bone doesn't // exist, the object isn't attached and NULL is returned. The returned TagPoint is only @@ -322,8 +350,6 @@ class ObjectAnimation : public Animation { public: ObjectAnimation(const MWWorld::Ptr& ptr, const std::string &model); - void addLight(const ESM::Light *light); - bool canBatch() const; void fillBatch(Ogre::StaticGeometry *sg); }; diff --git a/apps/openmw/mwrender/camera.cpp b/apps/openmw/mwrender/camera.cpp index 4580bae70..c7a27dfe8 100644 --- a/apps/openmw/mwrender/camera.cpp +++ b/apps/openmw/mwrender/camera.cpp @@ -7,7 +7,6 @@ #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" -#include "../mwbase/soundmanager.hpp" #include "../mwworld/ptr.hpp" #include "../mwworld/refdata.hpp" @@ -19,6 +18,7 @@ namespace MWRender Camera::Camera (Ogre::Camera *camera) : mCamera(camera), mCameraNode(NULL), + mCameraPosNode(NULL), mAnimation(NULL), mFirstPersonView(true), mPreviewMode(false), @@ -26,9 +26,8 @@ namespace MWRender mNearest(30.f), mFurthest(800.f), mIsNearest(false), - mIsFurthest(false), - mHeight(128.f), - mCameraDistance(300.f), + mHeight(124.f), + mCameraDistance(192.f), mDistanceAdjusted(false), mVanityToggleQueued(false), mViewModeToggleQueued(false) @@ -67,12 +66,16 @@ namespace MWRender } Ogre::Quaternion xr(Ogre::Radian(getPitch() + Ogre::Math::HALF_PI), Ogre::Vector3::UNIT_X); - if (!mVanity.enabled && !mPreviewMode) { - mCamera->getParentNode()->setOrientation(xr); - } else { + Ogre::Quaternion orient = xr; + if (mVanity.enabled || mPreviewMode) { Ogre::Quaternion zr(Ogre::Radian(getYaw()), Ogre::Vector3::UNIT_Z); - mCamera->getParentNode()->setOrientation(zr * xr); + orient = zr * xr; } + + if (isFirstPerson()) + mCamera->getParentNode()->setOrientation(orient); + else + mCameraNode->setOrientation(orient); } const std::string &Camera::getHandle() const @@ -80,34 +83,45 @@ namespace MWRender return mTrackingPtr.getRefData().getHandle(); } - void Camera::attachTo(const MWWorld::Ptr &ptr) + Ogre::SceneNode* Camera::attachTo(const MWWorld::Ptr &ptr) { mTrackingPtr = ptr; Ogre::SceneNode *node = mTrackingPtr.getRefData().getBaseNode()->createChildSceneNode(Ogre::Vector3(0.0f, 0.0f, mHeight)); + node->setInheritScale(false); + Ogre::SceneNode *posNode = node->createChildSceneNode(); + posNode->setInheritScale(false); if(mCameraNode) { node->setOrientation(mCameraNode->getOrientation()); - node->setPosition(mCameraNode->getPosition()); - node->setScale(mCameraNode->getScale()); + posNode->setPosition(mCameraPosNode->getPosition()); mCameraNode->getCreator()->destroySceneNode(mCameraNode); + mCameraNode->getCreator()->destroySceneNode(mCameraPosNode); } mCameraNode = node; - if(!mCamera->isAttached()) - mCameraNode->attachObject(mCamera); + mCameraPosNode = posNode; + + if (!isFirstPerson()) + { + mCamera->detachFromParent(); + mCameraPosNode->attachObject(mCamera); + } + + return mCameraPosNode; } - void Camera::updateListener() + void Camera::setPosition(const Ogre::Vector3& position) { - Ogre::Vector3 pos = mCamera->getRealPosition(); - Ogre::Vector3 dir = mCamera->getRealDirection(); - Ogre::Vector3 up = mCamera->getRealUp(); + mCameraPosNode->setPosition(position); + } - MWBase::Environment::get().getSoundManager()->setListenerPosDir(pos, dir, up); + void Camera::setPosition(float x, float y, float z) + { + setPosition(Ogre::Vector3(x,y,z)); } void Camera::update(float duration, bool paused) { - if (mAnimation->allowSwitchViewMode()) + if (mAnimation->upperBodyReady()) { // Now process the view changes we queued earlier if (mVanityToggleQueued) @@ -124,7 +138,6 @@ namespace MWRender } } - updateListener(); if (paused) return; @@ -144,7 +157,7 @@ namespace MWRender { // Changing the view will stop all playing animations, so if we are playing // anything important, queue the view change for later - if (!mAnimation->allowSwitchViewMode() && !force) + if (!mAnimation->upperBodyReady() && !force) { mViewModeToggleQueued = true; return; @@ -156,9 +169,9 @@ namespace MWRender processViewChange(); if (mFirstPersonView) { - mCamera->setPosition(0.f, 0.f, 0.f); + setPosition(0.f, 0.f, 0.f); } else { - mCamera->setPosition(0.f, 0.f, mCameraDistance); + setPosition(0.f, 0.f, mCameraDistance); } } @@ -173,7 +186,7 @@ namespace MWRender { // Changing the view will stop all playing animations, so if we are playing // anything important, queue the view change for later - if (!mPreviewMode) + if (isFirstPerson() && !mAnimation->upperBodyReady()) { mVanityToggleQueued = true; return false; @@ -192,14 +205,14 @@ namespace MWRender Ogre::Vector3 rot(0.f, 0.f, 0.f); if (mVanity.enabled) { rot.x = Ogre::Degree(-30.f).valueRadians(); - mMainCam.offset = mCamera->getPosition().z; + mMainCam.offset = mCameraPosNode->getPosition().z; } else { rot.x = getPitch(); offset = mMainCam.offset; } rot.z = getYaw(); - mCamera->setPosition(0.f, 0.f, offset); + setPosition(0.f, 0.f, offset); rotateCamera(rot, false); return true; @@ -207,7 +220,7 @@ namespace MWRender void Camera::togglePreviewMode(bool enable) { - if (mFirstPersonView && !mAnimation->allowSwitchViewMode()) + if (mFirstPersonView && !mAnimation->upperBodyReady()) return; if(mPreviewMode == enable) @@ -216,7 +229,7 @@ namespace MWRender mPreviewMode = enable; processViewChange(); - float offset = mCamera->getPosition().z; + float offset = mCameraPosNode->getPosition().z; if (mPreviewMode) { mMainCam.offset = offset; offset = mPreviewCam.offset; @@ -225,7 +238,7 @@ namespace MWRender offset = mMainCam.offset; } - mCamera->setPosition(0.f, 0.f, offset); + setPosition(0.f, 0.f, offset); } void Camera::setSneakOffset(float offset) @@ -284,7 +297,7 @@ namespace MWRender float Camera::getCameraDistance() const { - return mCamera->getPosition().z; + return mCameraPosNode->getPosition().z; } void Camera::setCameraDistance(float dist, bool adjust, bool override) @@ -292,23 +305,21 @@ namespace MWRender if(mFirstPersonView && !mPreviewMode && !mVanity.enabled) return; - mIsFurthest = false; mIsNearest = false; Ogre::Vector3 v(0.f, 0.f, dist); if (adjust) { - v += mCamera->getPosition(); + v += mCameraPosNode->getPosition(); } if (v.z >= mFurthest) { v.z = mFurthest; - mIsFurthest = true; } else if (!override && v.z < 10.f) { v.z = 10.f; } else if (override && v.z <= mNearest) { v.z = mNearest; mIsNearest = true; } - mCamera->setPosition(v); + setPosition(v); if (override) { if (mVanity.enabled || mPreviewMode) { @@ -325,9 +336,9 @@ namespace MWRender { if (mDistanceAdjusted) { if (mVanity.enabled || mPreviewMode) { - mCamera->setPosition(0, 0, mPreviewCam.offset); + setPosition(0, 0, mPreviewCam.offset); } else if (!mFirstPersonView) { - mCamera->setPosition(0, 0, mCameraDistance); + setPosition(0, 0, mCameraDistance); } } mDistanceAdjusted = false; @@ -358,10 +369,10 @@ namespace MWRender Ogre::TagPoint *tag = mAnimation->attachObjectToBone("Head", mCamera); tag->setInheritOrientation(false); } - else + else { mAnimation->setViewMode(NpcAnimation::VM_Normal); - mCameraNode->attachObject(mCamera); + mCameraPosNode->attachObject(mCamera); } rotateCamera(Ogre::Vector3(getPitch(), 0.f, getYaw()), false); } @@ -371,8 +382,7 @@ namespace MWRender mCamera->getParentSceneNode()->needUpdate(true); camera = mCamera->getRealPosition(); - focal = Ogre::Vector3((mCamera->getParentNode()->_getFullTransform() * - Ogre::Vector4(0.0f, 0.0f, 0.0f, 1.0f)).ptr()); + focal = mCameraNode->_getDerivedPosition(); } void Camera::togglePlayerLooking(bool enable) @@ -389,9 +399,4 @@ namespace MWRender { return mIsNearest; } - - bool Camera::isFurthest() - { - return mIsFurthest; - } } diff --git a/apps/openmw/mwrender/camera.hpp b/apps/openmw/mwrender/camera.hpp index 1e86bfb48..691a80862 100644 --- a/apps/openmw/mwrender/camera.hpp +++ b/apps/openmw/mwrender/camera.hpp @@ -27,6 +27,7 @@ namespace MWRender Ogre::Camera *mCamera; Ogre::SceneNode *mCameraNode; + Ogre::SceneNode *mCameraPosNode; NpcAnimation *mAnimation; @@ -36,7 +37,6 @@ namespace MWRender float mNearest; float mFurthest; bool mIsNearest; - bool mIsFurthest; struct { bool enabled, allowed; @@ -50,8 +50,8 @@ namespace MWRender bool mVanityToggleQueued; bool mViewModeToggleQueued; - /// Updates sound manager listener data - void updateListener(); + void setPosition(const Ogre::Vector3& position); + void setPosition(float x, float y, float z); public: Camera(Ogre::Camera *camera); @@ -73,7 +73,7 @@ namespace MWRender const std::string &getHandle() const; /// Attach camera to object - void attachTo(const MWWorld::Ptr &); + Ogre::SceneNode* attachTo(const MWWorld::Ptr &); /// @param Force view mode switch, even if currently not allowed by the animation. void toggleViewMode(bool force=false); @@ -118,8 +118,6 @@ namespace MWRender bool isVanityOrPreviewModeEnabled(); bool isNearest(); - - bool isFurthest(); }; } diff --git a/apps/openmw/mwrender/characterpreview.cpp b/apps/openmw/mwrender/characterpreview.cpp index 5e88b2250..756c79ad8 100644 --- a/apps/openmw/mwrender/characterpreview.cpp +++ b/apps/openmw/mwrender/characterpreview.cpp @@ -37,6 +37,7 @@ namespace MWRender , mViewport(NULL) , mCamera(NULL) , mNode(NULL) + , mRecover(false) { mCharacter.mCell = NULL; } @@ -46,6 +47,16 @@ namespace MWRender } + void CharacterPreview::onFrame() + { + if (mRecover) + { + setupRenderTarget(); + mRenderTarget->update(); + mRecover = false; + } + } + void CharacterPreview::setup () { mSceneMgr = Ogre::Root::getSingleton().createSceneManager(Ogre::ST_GENERIC); @@ -61,41 +72,33 @@ namespace MWRender l->setDirection (Ogre::Vector3(0.3, -0.7, 0.3)); l->setDiffuseColour (Ogre::ColourValue(1,1,1)); - mSceneMgr->setAmbientLight (Ogre::ColourValue(0.5, 0.5, 0.5)); + mSceneMgr->setAmbientLight (Ogre::ColourValue(0.25, 0.25, 0.25)); mCamera = mSceneMgr->createCamera (mName); + mCamera->setFOVy(Ogre::Degree(12.3)); mCamera->setAspectRatio (float(mSizeX) / float(mSizeY)); Ogre::SceneNode* renderRoot = mSceneMgr->getRootSceneNode()->createChildSceneNode("renderRoot"); - //we do this with mwRoot in renderingManager, do it here too. + // leftover of old coordinate system. TODO: remove this and adjust positions/orientations to match renderRoot->pitch(Ogre::Degree(-90)); mNode = renderRoot->createChildSceneNode(); mAnimation = new NpcAnimation(mCharacter, mNode, - 0, true, (renderHeadOnly() ? NpcAnimation::VM_HeadOnly : NpcAnimation::VM_Normal)); + 0, true, true, (renderHeadOnly() ? NpcAnimation::VM_HeadOnly : NpcAnimation::VM_Normal)); Ogre::Vector3 scale = mNode->getScale(); mCamera->setPosition(mPosition * scale); mCamera->lookAt(mLookAt * scale); - mCamera->setNearClipDistance (0.01); + mCamera->setNearClipDistance (1); mCamera->setFarClipDistance (1000); - mTexture = Ogre::TextureManager::getSingleton().getByName (mName); - if (mTexture.isNull ()) - mTexture = Ogre::TextureManager::getSingleton().createManual(mName, - Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, Ogre::TEX_TYPE_2D, mSizeX, mSizeY, 0, Ogre::PF_A8R8G8B8, Ogre::TU_RENDERTARGET); + mTexture = Ogre::TextureManager::getSingleton().createManual(mName, + Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, Ogre::TEX_TYPE_2D, mSizeX, mSizeY, 0, Ogre::PF_A8R8G8B8, Ogre::TU_RENDERTARGET, this); - mRenderTarget = mTexture->getBuffer()->getRenderTarget(); - mRenderTarget->removeAllViewports (); - mViewport = mRenderTarget->addViewport(mCamera); - mViewport->setOverlaysEnabled(false); - mViewport->setBackgroundColour(Ogre::ColourValue(0, 0, 0, 0)); - mViewport->setShadowsEnabled(false); - mRenderTarget->setActive(true); - mRenderTarget->setAutoUpdated (false); + setupRenderTarget(); onSetup (); } @@ -107,15 +110,16 @@ namespace MWRender mSceneMgr->destroyAllCameras(); delete mAnimation; Ogre::Root::getSingleton().destroySceneManager(mSceneMgr); + Ogre::TextureManager::getSingleton().remove(mName); } } void CharacterPreview::rebuild() { - assert(mAnimation); delete mAnimation; + mAnimation = NULL; mAnimation = new NpcAnimation(mCharacter, mNode, - 0, true, (renderHeadOnly() ? NpcAnimation::VM_HeadOnly : NpcAnimation::VM_Normal)); + 0, true, true, (renderHeadOnly() ? NpcAnimation::VM_HeadOnly : NpcAnimation::VM_Normal)); float scale=1.f; mCharacter.getClass().adjustScale(mCharacter, scale); @@ -127,12 +131,39 @@ namespace MWRender onSetup(); } + void CharacterPreview::loadResource(Ogre::Resource *resource) + { + Ogre::Texture* tex = dynamic_cast(resource); + if (!tex) + return; + + tex->createInternalResources(); + + mRenderTarget = NULL; + mViewport = NULL; + mRecover = true; + } + + void CharacterPreview::setupRenderTarget() + { + mRenderTarget = mTexture->getBuffer()->getRenderTarget(); + mRenderTarget->removeAllViewports (); + mViewport = mRenderTarget->addViewport(mCamera); + mViewport->setOverlaysEnabled(false); + mViewport->setBackgroundColour(Ogre::ColourValue(0, 0, 0, 0)); + mViewport->setShadowsEnabled(false); + mRenderTarget->setActive(true); + mRenderTarget->setAutoUpdated (false); + } + // -------------------------------------------------------------------------------------------------- InventoryPreview::InventoryPreview(MWWorld::Ptr character) - : CharacterPreview(character, 512, 1024, "CharacterPreview", Ogre::Vector3(0, 65, -180), Ogre::Vector3(0,65,0)) + : CharacterPreview(character, 512, 1024, "CharacterPreview", Ogre::Vector3(0, 71, -700), Ogre::Vector3(0,71,0)) , mSelectionBuffer(NULL) + , mSizeX(0) + , mSizeY(0) { } @@ -141,13 +172,31 @@ namespace MWRender delete mSelectionBuffer; } - void InventoryPreview::update(int sizeX, int sizeY) + void InventoryPreview::resize(int sizeX, int sizeY) + { + mSizeX = sizeX; + mSizeY = sizeY; + + mViewport->setDimensions (0, 0, std::min(1.f, float(mSizeX) / float(512)), std::min(1.f, float(mSizeY) / float(1024))); + mTexture->load(); + + if (!mRenderTarget) + setupRenderTarget(); + + mRenderTarget->update(); + } + + void InventoryPreview::update() { + if (!mAnimation) + return; + mAnimation->updateParts(); MWWorld::InventoryStore &inv = mCharacter.getClass().getInventoryStore(mCharacter); MWWorld::ContainerStoreIterator iter = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); std::string groupname; + bool showCarriedLeft = true; if(iter == inv.end()) groupname = "inventoryhandtohand"; else @@ -177,35 +226,49 @@ namespace MWRender groupname = "inventoryweapontwowide"; else groupname = "inventoryhandtohand"; - } + + showCarriedLeft = (iter->getClass().canBeEquipped(*iter, mCharacter).first != 2); + } else groupname = "inventoryhandtohand"; } + mAnimation->showCarriedLeft(showCarriedLeft); + mCurrentAnimGroup = groupname; mAnimation->play(mCurrentAnimGroup, 1, Animation::Group_All, false, 1.0f, "start", "stop", 0.0f, 0); MWWorld::ContainerStoreIterator torch = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); - if(torch != inv.end() && torch->getTypeName() == typeid(ESM::Light).name()) + if(torch != inv.end() && torch->getTypeName() == typeid(ESM::Light).name() && showCarriedLeft) { if(!mAnimation->getInfo("torch")) mAnimation->play("torch", 2, MWRender::Animation::Group_LeftArm, false, - 1.0f, "start", "stop", 0.0f, ~0ul); + 1.0f, "start", "stop", 0.0f, ~0ul, true); } else if(mAnimation->getInfo("torch")) mAnimation->disable("torch"); mAnimation->runAnimation(0.0f); - mViewport->setDimensions (0, 0, std::min(1.f, float(sizeX) / float(512)), std::min(1.f, float(sizeY) / float(1024))); - mNode->setOrientation (Ogre::Quaternion::IDENTITY); + mViewport->setDimensions (0, 0, std::min(1.f, float(mSizeX) / float(512)), std::min(1.f, float(mSizeY) / float(1024))); + mTexture->load(); + + if (!mRenderTarget) + setupRenderTarget(); + mRenderTarget->update(); mSelectionBuffer->update(); } + void InventoryPreview::setupRenderTarget() + { + CharacterPreview::setupRenderTarget(); + mViewport->setDimensions (0, 0, std::min(1.f, float(mSizeX) / float(512)), std::min(1.f, float(mSizeY) / float(1024))); + } + int InventoryPreview::getSlotSelected (int posX, int posY) { return mSelectionBuffer->getSelected (posX, posY); @@ -226,23 +289,30 @@ namespace MWRender RaceSelectionPreview::RaceSelectionPreview() : CharacterPreview(MWBase::Environment::get().getWorld()->getPlayerPtr(), - 512, 512, "CharacterHeadPreview", Ogre::Vector3(0, 6, -35), Ogre::Vector3(0,125,0)) + 512, 512, "CharacterHeadPreview", Ogre::Vector3(0, 8, -125), Ogre::Vector3(0,127,0)) + , mBase (*mCharacter.get()->mBase) , mRef(&mBase) + , mPitch(Ogre::Degree(6)) { - mBase = *mCharacter.get()->mBase; mCharacter = MWWorld::Ptr(&mRef, NULL); } void RaceSelectionPreview::update(float angle) { mAnimation->runAnimation(0.0f); - mNode->roll(Ogre::Radian(angle), Ogre::SceneNode::TS_LOCAL); + + mNode->setOrientation(Ogre::Quaternion(Ogre::Radian(angle), Ogre::Vector3::UNIT_Z) + * Ogre::Quaternion(mPitch, Ogre::Vector3::UNIT_X)); updateCamera(); } void RaceSelectionPreview::render() { + mTexture->load(); + + if (!mRenderTarget) + setupRenderTarget(); mRenderTarget->update(); } @@ -251,7 +321,8 @@ namespace MWRender mBase = proto; mBase.mId = "player"; rebuild(); - update(0); + mAnimation->runAnimation(0.0f); + updateCamera(); } void RaceSelectionPreview::onSetup () @@ -264,7 +335,10 @@ namespace MWRender void RaceSelectionPreview::updateCamera() { Ogre::Vector3 scale = mNode->getScale(); - Ogre::Vector3 headOffset = mAnimation->getNode("Bip01 Head")->_getDerivedPosition(); + Ogre::Node* headNode = mAnimation->getNode("Bip01 Head"); + if (!headNode) + return; + Ogre::Vector3 headOffset = headNode->_getDerivedPosition(); headOffset = mNode->convertLocalToWorldPosition(headOffset); mCamera->setPosition(headOffset + mPosition * scale); diff --git a/apps/openmw/mwrender/characterpreview.hpp b/apps/openmw/mwrender/characterpreview.hpp index 60312455f..80dbe18b4 100644 --- a/apps/openmw/mwrender/characterpreview.hpp +++ b/apps/openmw/mwrender/characterpreview.hpp @@ -22,7 +22,7 @@ namespace MWRender class NpcAnimation; - class CharacterPreview + class CharacterPreview : public Ogre::ManualResourceLoader { public: CharacterPreview(MWWorld::Ptr character, int sizeX, int sizeY, const std::string& name, @@ -34,6 +34,13 @@ namespace MWRender virtual void rebuild(); + void onFrame(); + + void loadResource(Ogre::Resource *resource); + + private: + bool mRecover; // Texture content was lost and needs to be re-rendered + private: CharacterPreview(const CharacterPreview&); CharacterPreview& operator=(const CharacterPreview&); @@ -41,6 +48,8 @@ namespace MWRender protected: virtual bool renderHeadOnly() { return false; } + virtual void setupRenderTarget(); + Ogre::TexturePtr mTexture; Ogre::RenderTarget* mRenderTarget; Ogre::Viewport* mViewport; @@ -72,11 +81,17 @@ namespace MWRender virtual ~InventoryPreview(); virtual void onSetup(); - void update(int sizeX, int sizeY); + void update(); // Render preview again, e.g. after changed equipment + void resize(int sizeX, int sizeY); int getSlotSelected(int posX, int posY); + protected: + virtual void setupRenderTarget(); + private: + int mSizeX; + int mSizeY; OEngine::Render::SelectionBuffer* mSelectionBuffer; }; @@ -105,6 +120,10 @@ namespace MWRender } void setPrototype(const ESM::NPC &proto); + + private: + + Ogre::Radian mPitch; }; } diff --git a/apps/openmw/mwrender/creatureanimation.cpp b/apps/openmw/mwrender/creatureanimation.cpp index f2447cb70..7260fc6d1 100644 --- a/apps/openmw/mwrender/creatureanimation.cpp +++ b/apps/openmw/mwrender/creatureanimation.cpp @@ -16,39 +16,37 @@ namespace MWRender { -CreatureAnimation::CreatureAnimation(const MWWorld::Ptr &ptr) +CreatureAnimation::CreatureAnimation(const MWWorld::Ptr &ptr, const std::string& model) : Animation(ptr, ptr.getRefData().getBaseNode()) { MWWorld::LiveCellRef *ref = mPtr.get(); - std::string model = ptr.getClass().getModel(ptr); if(!model.empty()) { setObjectRoot(model, false); setRenderProperties(mObjectRoot, RV_Actors, RQG_Main, RQG_Alpha); if((ref->mBase->mFlags&ESM::Creature::Bipedal)) - addAnimSource("meshes\\base_anim.nif"); + addAnimSource("meshes\\xbase_anim.nif"); addAnimSource(model); } } -CreatureWeaponAnimation::CreatureWeaponAnimation(const MWWorld::Ptr &ptr) +CreatureWeaponAnimation::CreatureWeaponAnimation(const MWWorld::Ptr &ptr, const std::string& model) : Animation(ptr, ptr.getRefData().getBaseNode()) , mShowWeapons(false) , mShowCarriedLeft(false) { MWWorld::LiveCellRef *ref = mPtr.get(); - std::string model = ptr.getClass().getModel(ptr); if(!model.empty()) { setObjectRoot(model, false); setRenderProperties(mObjectRoot, RV_Actors, RQG_Main, RQG_Alpha); if((ref->mBase->mFlags&ESM::Creature::Bipedal)) - addAnimSource("meshes\\base_anim.nif"); + addAnimSource("meshes\\xbase_anim.nif"); addAnimSource(model); mPtr.getClass().getInventoryStore(mPtr).setListener(this, mPtr); @@ -90,6 +88,9 @@ void CreatureWeaponAnimation::updateParts() void CreatureWeaponAnimation::updatePart(NifOgre::ObjectScenePtr& scene, int slot) { + if (!mSkelBase) + return; + MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); MWWorld::ContainerStoreIterator it = inv.getSlot(slot); @@ -106,7 +107,7 @@ void CreatureWeaponAnimation::updatePart(NifOgre::ObjectScenePtr& scene, int slo else bonename = "Shield Bone"; - scene = NifOgre::Loader::createObjects(mSkelBase, bonename, mInsert, item.getClass().getModel(item)); + scene = NifOgre::Loader::createObjects(mSkelBase, bonename, bonename, mInsert, item.getClass().getModel(item)); Ogre::Vector3 glowColor = getEnchantmentColor(item); setRenderProperties(scene, RV_Actors, RQG_Main, RQG_Alpha, 0, @@ -150,7 +151,7 @@ void CreatureWeaponAnimation::updatePart(NifOgre::ObjectScenePtr& scene, int slo } std::vector >::iterator ctrl(scene->mControllers.begin()); - for(;ctrl != scene->mControllers.end();ctrl++) + for(;ctrl != scene->mControllers.end();++ctrl) { if(ctrl->getSource().isNull()) { @@ -183,7 +184,9 @@ void CreatureWeaponAnimation::releaseArrow() Ogre::Vector3 CreatureWeaponAnimation::runAnimation(float duration) { Ogre::Vector3 ret = Animation::runAnimation(duration); - pitchSkeleton(mPtr.getRefData().getPosition().rot[0], mSkelBase->getSkeleton()); + + if (mSkelBase) + pitchSkeleton(mPtr.getRefData().getPosition().rot[0], mSkelBase->getSkeleton()); if (!mWeapon.isNull()) { diff --git a/apps/openmw/mwrender/creatureanimation.hpp b/apps/openmw/mwrender/creatureanimation.hpp index d6cd8a517..6201c7af4 100644 --- a/apps/openmw/mwrender/creatureanimation.hpp +++ b/apps/openmw/mwrender/creatureanimation.hpp @@ -15,7 +15,7 @@ namespace MWRender class CreatureAnimation : public Animation { public: - CreatureAnimation(const MWWorld::Ptr& ptr); + CreatureAnimation(const MWWorld::Ptr& ptr, const std::string &model); virtual ~CreatureAnimation() {} }; @@ -25,7 +25,7 @@ namespace MWRender class CreatureWeaponAnimation : public Animation, public WeaponAnimation, public MWWorld::InventoryStoreListener { public: - CreatureWeaponAnimation(const MWWorld::Ptr& ptr); + CreatureWeaponAnimation(const MWWorld::Ptr& ptr, const std::string &model); virtual ~CreatureWeaponAnimation() {} virtual void equipmentChanged() { updateParts(); } diff --git a/apps/openmw/mwrender/debugging.cpp b/apps/openmw/mwrender/debugging.cpp index 4f5536ca3..972c1b6dd 100644 --- a/apps/openmw/mwrender/debugging.cpp +++ b/apps/openmw/mwrender/debugging.cpp @@ -231,8 +231,9 @@ void Debugging::togglePathgrid() void Debugging::enableCellPathgrid(MWWorld::CellStore *store) { + MWBase::World* world = MWBase::Environment::get().getWorld(); const ESM::Pathgrid *pathgrid = - MWBase::Environment::get().getWorld()->getStore().get().search(*store->getCell()); + world->getStore().get().search(*store->getCell()); if (!pathgrid) return; Vector3 cellPathGridPos(0, 0, 0); diff --git a/apps/openmw/mwrender/effectmanager.cpp b/apps/openmw/mwrender/effectmanager.cpp index 968be0f9e..503a0223e 100644 --- a/apps/openmw/mwrender/effectmanager.cpp +++ b/apps/openmw/mwrender/effectmanager.cpp @@ -1,5 +1,7 @@ #include "effectmanager.hpp" +#include + #include #include #include @@ -21,19 +23,9 @@ void EffectManager::addEffect(const std::string &model, std::string textureOverr Ogre::SceneNode* sceneNode = mSceneMgr->getRootSceneNode()->createChildSceneNode(worldPosition); sceneNode->setScale(scale,scale,scale); - // fix texture extension to .dds - if (textureOverride.size() > 4) - { - textureOverride[textureOverride.size()-3] = 'd'; - textureOverride[textureOverride.size()-2] = 'd'; - textureOverride[textureOverride.size()-1] = 's'; - } - - NifOgre::ObjectScenePtr scene = NifOgre::Loader::createObjects(sceneNode, model); - // TODO: turn off shadow casting - MWRender::Animation::setRenderProperties(scene, RV_Misc, + MWRender::Animation::setRenderProperties(scene, RV_Effects, RQG_Main, RQG_Alpha, 0.f, false, NULL); for(size_t i = 0;i < scene->mControllers.size();i++) @@ -44,6 +36,7 @@ void EffectManager::addEffect(const std::string &model, std::string textureOverr if (!textureOverride.empty()) { + std::string correctedTexture = Misc::ResourceHelpers::correctTexturePath(textureOverride); for(size_t i = 0;i < scene->mParticles.size(); ++i) { Ogre::ParticleSystem* partSys = scene->mParticles[i]; @@ -59,7 +52,7 @@ void EffectManager::addEffect(const std::string &model, std::string textureOverr for (int tex=0; texgetNumTextureUnitStates(); ++tex) { Ogre::TextureUnitState* tus = pass->getTextureUnitState(tex); - tus->setTextureName("textures\\" + textureOverride); + tus->setTextureName(correctedTexture); } } } diff --git a/apps/openmw/mwrender/globalmap.cpp b/apps/openmw/mwrender/globalmap.cpp index 1ccfd9527..de78748b5 100644 --- a/apps/openmw/mwrender/globalmap.cpp +++ b/apps/openmw/mwrender/globalmap.cpp @@ -11,6 +11,7 @@ #include #include +#include #include @@ -29,8 +30,13 @@ namespace MWRender , mWidth(0) , mHeight(0) { + mCellSize = Settings::Manager::getInt("global map cell size", "Map"); } + GlobalMap::~GlobalMap() + { + Ogre::TextureManager::getSingleton().remove(mOverlayTexture->getName()); + } void GlobalMap::render (Loading::Listener* loadingListener) { @@ -53,124 +59,96 @@ namespace MWRender mMaxY = it->getGridY(); } - int cellSize = 24; - mWidth = cellSize*(mMaxX-mMinX+1); - mHeight = cellSize*(mMaxY-mMinY+1); + mWidth = mCellSize*(mMaxX-mMinX+1); + mHeight = mCellSize*(mMaxY-mMinY+1); loadingListener->loadingOn(); loadingListener->setLabel("Creating map"); loadingListener->setProgressRange((mMaxX-mMinX+1) * (mMaxY-mMinY+1)); loadingListener->setProgress(0); - const Ogre::ColourValue waterShallowColour(0.15, 0.2, 0.19); - const Ogre::ColourValue waterDeepColour(0.1, 0.14, 0.13); - const Ogre::ColourValue groundColour(0.254, 0.19, 0.13); - const Ogre::ColourValue mountainColour(0.05, 0.05, 0.05); - const Ogre::ColourValue hillColour(0.16, 0.12, 0.08); + std::vector data (mWidth * mHeight * 3); - //if (!boost::filesystem::exists(mCacheDir + "/GlobalMap.png")) - if (1) + for (int x = mMinX; x <= mMaxX; ++x) { - std::vector data (mWidth * mHeight * 3); - - for (int x = mMinX; x <= mMaxX; ++x) + for (int y = mMinY; y <= mMaxY; ++y) { - for (int y = mMinY; y <= mMaxY; ++y) - { - ESM::Land* land = esmStore.get().search (x,y); + ESM::Land* land = esmStore.get().search (x,y); - if (land) - { - int mask = ESM::Land::DATA_VHGT | ESM::Land::DATA_VNML | ESM::Land::DATA_VCLR | ESM::Land::DATA_VTEX; - if (!land->isDataLoaded(mask)) - land->loadData(mask); - } + if (land) + { + int mask = ESM::Land::DATA_WNAM; + if (!land->isDataLoaded(mask)) + land->loadData(mask); + } - for (int cellY=0; cellYmLandData->mHeights[vertexY * ESM::Land::LAND_SIZE + vertexX]; - const float mountainHeight = 15000.f; - const float hillHeight = 2500.f; - - if (landHeight >= 0) - { - if (landHeight >= hillHeight) - { - float factor = std::min(1.f, float(landHeight-hillHeight)/mountainHeight); - r = (hillColour.r * (1-factor) + mountainColour.r * factor) * 255; - g = (hillColour.g * (1-factor) + mountainColour.g * factor) * 255; - b = (hillColour.b * (1-factor) + mountainColour.b * factor) * 255; - } - else - { - float factor = std::min(1.f, float(landHeight)/hillHeight); - r = (groundColour.r * (1-factor) + hillColour.r * factor) * 255; - g = (groundColour.g * (1-factor) + hillColour.g * factor) * 255; - b = (groundColour.b * (1-factor) + hillColour.b * factor) * 255; - } - } - else - { - if (landHeight >= -100) - { - float factor = std::min(1.f, -1*landHeight/100.f); - r = (((waterShallowColour+groundColour)/2).r * (1-factor) + waterShallowColour.r * factor) * 255; - g = (((waterShallowColour+groundColour)/2).g * (1-factor) + waterShallowColour.g * factor) * 255; - b = (((waterShallowColour+groundColour)/2).b * (1-factor) + waterShallowColour.b * factor) * 255; - } - else - { - float factor = std::min(1.f, -1*(landHeight-100)/1000.f); - r = (waterShallowColour.r * (1-factor) + waterDeepColour.r * factor) * 255; - g = (waterShallowColour.g * (1-factor) + waterDeepColour.g * factor) * 255; - b = (waterShallowColour.b * (1-factor) + waterDeepColour.b * factor) * 255; - } - } + unsigned char r,g,b; - } + float y = 0; + if (land && land->mDataTypes & ESM::Land::DATA_WNAM) + y = (land->mLandData->mWnam[vertexY * 9 + vertexX] << 4) / 2048.f; + else + y = (SCHAR_MIN << 4) / 2048.f; + if (y < 0) + { + r = (14 * y + 38); + g = 20 * y + 56; + b = 18 * y + 51; + } + else if (y < 0.3f) + { + if (y < 0.1f) + y *= 8.f; else { - r = waterDeepColour.r * 255; - g = waterDeepColour.g * 255; - b = waterDeepColour.b * 255; + y -= 0.1; + y += 0.8; } - - data[texelY * mWidth * 3 + texelX * 3] = r; - data[texelY * mWidth * 3 + texelX * 3+1] = g; - data[texelY * mWidth * 3 + texelX * 3+2] = b; + r = 66 - 32 * y; + g = 48 - 23 * y; + b = 33 - 16 * y; + } + else + { + y -= 0.3f; + y *= 1.428f; + r = 34 - 29 * y; + g = 25 - 20 * y; + b = 17 - 12 * y; } + + data[texelY * mWidth * 3 + texelX * 3] = r; + data[texelY * mWidth * 3 + texelX * 3+1] = g; + data[texelY * mWidth * 3 + texelX * 3+2] = b; } - loadingListener->increaseProgress(1); } + loadingListener->increaseProgress(); + if (land) + land->unloadData(); } + } - Ogre::DataStreamPtr stream(new Ogre::MemoryDataStream(&data[0], data.size())); + Ogre::DataStreamPtr stream(new Ogre::MemoryDataStream(&data[0], data.size())); - tex = Ogre::TextureManager::getSingleton ().createManual ("GlobalMap.png", Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, - Ogre::TEX_TYPE_2D, mWidth, mHeight, 0, Ogre::PF_B8G8R8, Ogre::TU_STATIC); - tex->loadRawData(stream, mWidth, mHeight, Ogre::PF_B8G8R8); - } - else - tex = Ogre::TextureManager::getSingleton ().getByName ("GlobalMap.png"); + tex = Ogre::TextureManager::getSingleton ().createManual ("GlobalMap.png", Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, + Ogre::TEX_TYPE_2D, mWidth, mHeight, 0, Ogre::PF_B8G8R8, Ogre::TU_STATIC); + tex->loadRawData(stream, mWidth, mHeight, Ogre::PF_B8G8R8); tex->load(); - mOverlayTexture = Ogre::TextureManager::getSingleton().createManual("GlobalMapOverlay", Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, - Ogre::TEX_TYPE_2D, mWidth, mHeight, 0, Ogre::PF_A8B8G8R8, Ogre::TU_DYNAMIC_WRITE_ONLY); + Ogre::TEX_TYPE_2D, mWidth, mHeight, 0, Ogre::PF_A8B8G8R8, Ogre::TU_DYNAMIC, this); clear(); @@ -194,9 +172,9 @@ namespace MWRender void GlobalMap::exploreCell(int cellX, int cellY) { - float originX = (cellX - mMinX) * 24; + float originX = (cellX - mMinX) * mCellSize; // NB y + 1, because we want the top left corner, not bottom left where the origin of the cell is - float originY = mHeight - (cellY+1 - mMinY) * 24; + float originY = mHeight - (cellY+1 - mMinY) * mCellSize; if (cellX > mMaxX || cellX < mMinX || cellY > mMaxY || cellY < mMinY) return; @@ -204,31 +182,49 @@ namespace MWRender Ogre::TexturePtr localMapTexture = Ogre::TextureManager::getSingleton().getByName("Cell_" + boost::lexical_cast(cellX) + "_" + boost::lexical_cast(cellY)); - // mipmap version - can't get ogre to generate automips.. - /*if (!localMapTexture.isNull()) + if (!localMapTexture.isNull()) { - assert(localMapTexture->getBuffer(0, 4)->getWidth() == 64); // 1024 / 2^4 + int mapWidth = localMapTexture->getWidth(); + int mapHeight = localMapTexture->getHeight(); + mOverlayTexture->load(); + mOverlayTexture->getBuffer()->blit(localMapTexture->getBuffer(), Ogre::Image::Box(0,0,mapWidth,mapHeight), + Ogre::Image::Box(originX,originY,originX+mCellSize,originY+mCellSize)); - mOverlayTexture->getBuffer()->blit(localMapTexture->getBuffer(0, 4), Ogre::Image::Box(0,0,64, 64), - Ogre::Image::Box(originX,originY,originX+24,originY+24)); - }*/ + Ogre::Image backup; + std::vector data; + data.resize(mCellSize*mCellSize*4, 0); + backup.loadDynamicImage(&data[0], mCellSize, mCellSize, Ogre::PF_A8B8G8R8); - if (!localMapTexture.isNull()) - { - mOverlayTexture->getBuffer()->blit(localMapTexture->getBuffer(), Ogre::Image::Box(0,0,512,512), - Ogre::Image::Box(originX,originY,originX+24,originY+24)); + localMapTexture->getBuffer()->blitToMemory(Ogre::Image::Box(0,0,mapWidth,mapHeight), backup.getPixelBox()); + + for (int x=0; x buffer; - // initialize to (0,0,0,0) - buffer.resize(mWidth * mHeight, 0); + Ogre::uchar* buffer = OGRE_ALLOC_T(Ogre::uchar, mWidth * mHeight * 4, Ogre::MEMCATEGORY_GENERAL); + memset(buffer, 0, mWidth * mHeight * 4); - Ogre::PixelBox pb(mWidth, mHeight, 1, Ogre::PF_A8B8G8R8, &buffer[0]); + mOverlayImage.loadDynamicImage(&buffer[0], mWidth, mHeight, 1, Ogre::PF_A8B8G8R8, true); // pass ownership of buffer to image - mOverlayTexture->getBuffer()->blitFromMemory(pb); + mOverlayTexture->load(); + } + + void GlobalMap::loadResource(Ogre::Resource *resource) + { + Ogre::Texture* tex = static_cast(resource); + Ogre::ConstImagePtrList list; + list.push_back(&mOverlayImage); + tex->_loadImages(list); } void GlobalMap::write(ESM::GlobalMap& map) @@ -238,9 +234,7 @@ namespace MWRender map.mBounds.mMinY = mMinY; map.mBounds.mMaxY = mMaxY; - Ogre::Image image; - mOverlayTexture->convertToImage(image); - Ogre::DataStreamPtr encoded = image.encode("png"); + Ogre::DataStreamPtr encoded = mOverlayImage.encode("png"); map.mImageData.resize(encoded->size()); encoded->read(&map.mImageData[0], encoded->size()); } @@ -249,9 +243,9 @@ namespace MWRender { const ESM::GlobalMap::Bounds& bounds = map.mBounds; - if (bounds.mMaxX-bounds.mMinX <= 0) + if (bounds.mMaxX-bounds.mMinX < 0) return; - if (bounds.mMaxY-bounds.mMinY <= 0) + if (bounds.mMaxY-bounds.mMinY < 0) return; if (bounds.mMinX > bounds.mMaxX @@ -273,7 +267,7 @@ namespace MWRender // If cell bounds of the currently loaded content and the loaded savegame do not match, // we need to resize source/dest boxes to accommodate // This means nonexisting cells will be dropped silently - int cellImageSizeDst = 24; + int cellImageSizeDst = mCellSize; // Completely off-screen? -> no need to blit anything if (bounds.mMaxX < mMinX @@ -303,8 +297,16 @@ namespace MWRender image.getHeight(), 0, Ogre::PF_A8B8G8R8); tex->loadImage(image); + mOverlayTexture->load(); mOverlayTexture->getBuffer()->blit(tex->getBuffer(), srcBox, destBox); + if (srcBox.left == destBox.left && srcBox.right == destBox.right + && srcBox.top == destBox.top && srcBox.bottom == destBox.bottom + && int(image.getWidth()) == mWidth && int(image.getHeight()) == mHeight) + mOverlayImage = image; + else + mOverlayTexture->convertToImage(mOverlayImage); + Ogre::TextureManager::getSingleton().remove("@temp"); } } diff --git a/apps/openmw/mwrender/globalmap.hpp b/apps/openmw/mwrender/globalmap.hpp index 6075d042e..b3ae85b11 100644 --- a/apps/openmw/mwrender/globalmap.hpp +++ b/apps/openmw/mwrender/globalmap.hpp @@ -18,24 +18,27 @@ namespace ESM namespace MWRender { - class GlobalMap + class GlobalMap : public Ogre::ManualResourceLoader { public: GlobalMap(const std::string& cacheDir); + ~GlobalMap(); void render(Loading::Listener* loadingListener); - int getWidth() { return mWidth; } - int getHeight() { return mHeight; } + int getWidth() const { return mWidth; } + int getHeight() const { return mHeight; } + + int getCellSize() const { return mCellSize; } void worldPosToImageSpace(float x, float z, float& imageX, float& imageY); - ///< @param x x ogre coords - /// @param z z ogre coords void cellTopLeftCornerToImageSpace(int x, int y, float& imageX, float& imageY); void exploreCell (int cellX, int cellY); + virtual void loadResource(Ogre::Resource* resource); + /// Clears the overlay void clear(); @@ -45,9 +48,12 @@ namespace MWRender private: std::string mCacheDir; + int mCellSize; + std::vector< std::pair > mExploredCells; Ogre::TexturePtr mOverlayTexture; + Ogre::Image mOverlayImage; // Backup in system memory int mWidth; int mHeight; diff --git a/apps/openmw/mwrender/localmap.cpp b/apps/openmw/mwrender/localmap.cpp index 271cbbfe1..638a08623 100644 --- a/apps/openmw/mwrender/localmap.cpp +++ b/apps/openmw/mwrender/localmap.cpp @@ -24,8 +24,10 @@ using namespace MWRender; using namespace Ogre; -LocalMap::LocalMap(OEngine::Render::OgreRenderer* rend, MWRender::RenderingManager* rendering) : - mInterior(false), mCellX(0), mCellY(0) +LocalMap::LocalMap(OEngine::Render::OgreRenderer* rend, MWRender::RenderingManager* rendering) + : mInterior(false) + , mAngle(0.f) + , mMapResolution(Settings::Manager::getInt("local map resolution", "Map")) { mRendering = rend; mRenderingManager = rendering; @@ -49,7 +51,7 @@ LocalMap::LocalMap(OEngine::Render::OgreRenderer* rend, MWRender::RenderingManag "localmap/rtt", ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, TEX_TYPE_2D, - sMapResolution, sMapResolution, + mMapResolution, mMapResolution, 0, PF_R8G8B8, TU_RENDERTARGET); @@ -98,6 +100,7 @@ void LocalMap::saveFogOfWar(MWWorld::CellStore* cell) return; Ogre::Image image; + tex->load(); tex->convertToImage(image); Ogre::DataStreamPtr encoded = image.encode("tga"); @@ -137,6 +140,7 @@ void LocalMap::saveFogOfWar(MWWorld::CellStore* cell) return; Ogre::Image image; + tex->load(); tex->convertToImage(image); fog->mFogTextures.push_back(ESM::FogTexture()); @@ -196,7 +200,7 @@ void LocalMap::requestMap(MWWorld::CellStore* cell, // Get the cell's NorthMarker rotation. This is used to rotate the entire map. const Vector2& north = MWBase::Environment::get().getWorld()->getNorthVector(cell); - Radian angle = Ogre::Math::ATan2 (north.x, north.y) + Ogre::Degree(2); + Radian angle = Ogre::Math::ATan2 (north.x, north.y); mAngle = angle.valueRadians(); // Rotate the cell and merge the rotated corners to the bounding box @@ -322,6 +326,7 @@ void LocalMap::createFogOfWar(const std::string& texturePrefix) buffer.resize(sFogOfWarResolution*sFogOfWarResolution, 0xFF000000); // upload to the texture + tex->load(); memcpy(tex->getBuffer()->lock(HardwareBuffer::HBL_DISCARD), &buffer[0], sFogOfWarResolution*sFogOfWarResolution*4); tex->getBuffer()->unlock(); @@ -409,7 +414,7 @@ void LocalMap::render(const float x, const float y, texture, ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, TEX_TYPE_2D, - sMapResolution, sMapResolution, + mMapResolution, mMapResolution, 0, PF_R8G8B8); tex->getBuffer()->blit(mRenderTexture->getBuffer()); @@ -428,7 +433,7 @@ void LocalMap::render(const float x, const float y, mRendering->getScene()->setAmbientLight(oldAmbient); } -void LocalMap::getInteriorMapPosition (Ogre::Vector2 pos, float& nX, float& nY, int& x, int& y) +void LocalMap::worldToInteriorMapPosition (Ogre::Vector2 pos, float& nX, float& nY, int& x, int& y) { pos = rotatePoint(pos, Vector2(mBounds.getCenter().x, mBounds.getCenter().y), mAngle); @@ -441,6 +446,18 @@ void LocalMap::getInteriorMapPosition (Ogre::Vector2 pos, float& nX, float& nY, nY = 1.0-(pos.y - min.y - sSize*y)/sSize; } +Ogre::Vector2 LocalMap::interiorMapToWorldPosition (float nX, float nY, int x, int y) +{ + Vector2 min(mBounds.getMinimum().x, mBounds.getMinimum().y); + Ogre::Vector2 pos; + + pos.x = sSize * (nX + x) + min.x; + pos.y = sSize * (1.0-nY + y) + min.y; + + pos = rotatePoint(pos, Vector2(mBounds.getCenter().x, mBounds.getCenter().y), -mAngle); + return pos; +} + bool LocalMap::isPositionExplored (float nX, float nY, int x, int y, bool interior) { std::string texName = (interior ? mInteriorName + "_" : "Cell_") + coordStr(x, y); @@ -477,7 +494,7 @@ void LocalMap::loadResource(Ogre::Resource* resource) std::vector& buffer = mBuffers[resourceName]; - Ogre::Texture* tex = dynamic_cast(resource); + Ogre::Texture* tex = static_cast(resource); tex->createInternalResources(); memcpy(tex->getBuffer()->lock(HardwareBuffer::HBL_DISCARD), &buffer[0], sFogOfWarResolution*sFogOfWarResolution*4); tex->getBuffer()->unlock(); @@ -499,7 +516,7 @@ void LocalMap::updatePlayer (const Ogre::Vector3& position, const Ogre::Quaterni Vector2 pos(position.x, position.y); if (mInterior) - getInteriorMapPosition(pos, u,v, x,y); + worldToInteriorMapPosition(pos, u,v, x,y); Vector3 playerdirection = mCameraRotNode->convertWorldToLocalOrientation(orientation).yAxis(); @@ -507,10 +524,9 @@ void LocalMap::updatePlayer (const Ogre::Vector3& position, const Ogre::Quaterni { x = std::ceil(pos.x / sSize)-1; y = std::ceil(pos.y / sSize)-1; - mCellX = x; - mCellY = y; } - MWBase::Environment::get().getWindowManager()->setActiveMap(x,y,mInterior); + else + MWBase::Environment::get().getWindowManager()->setActiveMap(x,y,mInterior); // convert from world coordinates to texture UV coordinates std::string texBaseName; @@ -525,7 +541,7 @@ void LocalMap::updatePlayer (const Ogre::Vector3& position, const Ogre::Quaterni texBaseName = mInteriorName + "_"; } - MWBase::Environment::get().getWindowManager()->setPlayerPos(u, v); + MWBase::Environment::get().getWindowManager()->setPlayerPos(x, y, u, v); MWBase::Environment::get().getWindowManager()->setPlayerDir(playerdirection.x, playerdirection.y); // explore radius (squared) @@ -581,6 +597,8 @@ void LocalMap::updatePlayer (const Ogre::Vector3& position, const Ogre::Quaterni } } + tex->load(); + // copy to the texture // NOTE: Could be optimized later. We actually only need to update the region that changed. // Not a big deal at the moment, the FoW is only 32x32 anyway. diff --git a/apps/openmw/mwrender/localmap.hpp b/apps/openmw/mwrender/localmap.hpp index 1572800e5..7cfa38814 100644 --- a/apps/openmw/mwrender/localmap.hpp +++ b/apps/openmw/mwrender/localmap.hpp @@ -61,8 +61,6 @@ namespace MWRender * Set the position & direction of the player. * @remarks This is used to draw a "fog of war" effect * to hide areas on the map the player has not discovered yet. - * @param position (OGRE coordinates) - * @param camera orientation (OGRE coordinates) */ void updatePlayer (const Ogre::Vector3& position, const Ogre::Quaternion& orientation); @@ -74,9 +72,11 @@ namespace MWRender /** * Get the interior map texture index and normalized position - * on this texture, given a world position (in ogre coordinates) + * on this texture, given a world position */ - void getInteriorMapPosition (Ogre::Vector2 pos, float& nX, float& nY, int& x, int& y); + void worldToInteriorMapPosition (Ogre::Vector2 pos, float& nX, float& nY, int& x, int& y); + + Ogre::Vector2 interiorMapToWorldPosition (float nX, float nY, int x, int y); /** * Check if a given position is explored by the player (i.e. not obscured by fog of war) @@ -87,7 +87,7 @@ namespace MWRender OEngine::Render::OgreRenderer* mRendering; MWRender::RenderingManager* mRenderingManager; - static const int sMapResolution = 512; + int mMapResolution; // the dynamic texture is a bottleneck, so don't set this too high static const int sFogOfWarResolution = 32; @@ -134,7 +134,6 @@ namespace MWRender Ogre::RenderTarget* mRenderTarget; bool mInterior; - int mCellX, mCellY; Ogre::AxisAlignedBox mBounds; std::string mInteriorName; }; diff --git a/apps/openmw/mwrender/npcanimation.cpp b/apps/openmw/mwrender/npcanimation.cpp index be2b262fc..66ba25859 100644 --- a/apps/openmw/mwrender/npcanimation.cpp +++ b/apps/openmw/mwrender/npcanimation.cpp @@ -12,6 +12,8 @@ #include +#include + #include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/class.hpp" @@ -56,8 +58,28 @@ std::string getVampireHead(const std::string& race, bool female) } } - assert(sVampireMapping[thisCombination]); - return "meshes\\" + sVampireMapping[thisCombination]->mModel; + if (sVampireMapping.find(thisCombination) == sVampireMapping.end()) + sVampireMapping[thisCombination] = NULL; + + const ESM::BodyPart* bodyPart = sVampireMapping[thisCombination]; + if (!bodyPart) + return std::string(); + return "meshes\\" + bodyPart->mModel; +} + +bool isSkinned (NifOgre::ObjectScenePtr scene) +{ + if (scene->mSkelBase == NULL) + return false; + for(size_t j = 0; j < scene->mEntities.size(); j++) + { + Ogre::Entity *ent = scene->mEntities[j]; + if(scene->mSkelBase != ent && ent->hasSkeleton()) + { + return true; + } + } + return false; } } @@ -66,22 +88,81 @@ std::string getVampireHead(const std::string& race, bool female) namespace MWRender { -float HeadAnimationTime::getValue() const +HeadAnimationTime::HeadAnimationTime(MWWorld::Ptr reference) + : mReference(reference), mTalkStart(0), mTalkStop(0), mBlinkStart(0), mBlinkStop(0), mValue(0), mEnabled(true) +{ + resetBlinkTimer(); +} + +void HeadAnimationTime::setEnabled(bool enabled) { - // TODO use time from text keys (Talk Start/Stop, Blink Start/Stop) - // TODO: Handle eye blinking + mEnabled = enabled; +} + +void HeadAnimationTime::resetBlinkTimer() +{ + mBlinkTimer = -(2 + (std::rand() / double(RAND_MAX*1.0)) * 6); +} + +void HeadAnimationTime::update(float dt) +{ + if (!mEnabled) + return; + if (MWBase::Environment::get().getSoundManager()->sayDone(mReference)) - return 0; + { + mBlinkTimer += dt; + + float duration = mBlinkStop - mBlinkStart; + + if (mBlinkTimer >= 0 && mBlinkTimer <= duration) + { + mValue = mBlinkStart + mBlinkTimer; + } + else + mValue = mBlinkStop; + + if (mBlinkTimer > duration) + resetBlinkTimer(); + } else - // TODO: Use the loudness of the currently playing sound - return 1; + { + mValue = mTalkStart + + (mTalkStop - mTalkStart) * + std::min(1.f, MWBase::Environment::get().getSoundManager()->getSaySoundLoudness(mReference)*2); // Rescale a bit (most voices are not very loud) + } +} + +float HeadAnimationTime::getValue() const +{ + return mValue; +} + +void HeadAnimationTime::setTalkStart(float value) +{ + mTalkStart = value; +} + +void HeadAnimationTime::setTalkStop(float value) +{ + mTalkStop = value; +} + +void HeadAnimationTime::setBlinkStart(float value) +{ + mBlinkStart = value; +} + +void HeadAnimationTime::setBlinkStop(float value) +{ + mBlinkStop = value; } static NpcAnimation::PartBoneMap createPartListMap() { NpcAnimation::PartBoneMap result; result.insert(std::make_pair(ESM::PRT_Head, "Head")); - result.insert(std::make_pair(ESM::PRT_Hair, "Head")); + result.insert(std::make_pair(ESM::PRT_Hair, "Head")); // note it uses "Head" as attach bone, but "Hair" as filter result.insert(std::make_pair(ESM::PRT_Neck, "Neck")); result.insert(std::make_pair(ESM::PRT_Cuirass, "Chest")); result.insert(std::make_pair(ESM::PRT_Groin, "Groin")); @@ -118,7 +199,7 @@ NpcAnimation::~NpcAnimation() } -NpcAnimation::NpcAnimation(const MWWorld::Ptr& ptr, Ogre::SceneNode* node, int visibilityFlags, bool disableListener, ViewMode viewMode) +NpcAnimation::NpcAnimation(const MWWorld::Ptr& ptr, Ogre::SceneNode* node, int visibilityFlags, bool disableListener, bool disableSounds, ViewMode viewMode) : Animation(ptr, node), mVisibilityFlags(visibilityFlags), mListenerDisabled(disableListener), @@ -127,7 +208,10 @@ NpcAnimation::NpcAnimation(const MWWorld::Ptr& ptr, Ogre::SceneNode* node, int v mShowCarriedLeft(true), mFirstPersonOffset(0.f, 0.f, 0.f), mAlpha(1.f), - mNpcType(Type_Normal) + mNpcType(Type_Normal), + mSoundsDisabled(disableSounds), + mHeadPitch(0.f), + mHeadYaw(0.f) { mNpc = mPtr.get()->mBase; @@ -140,10 +224,10 @@ NpcAnimation::NpcAnimation(const MWWorld::Ptr& ptr, Ogre::SceneNode* node, int v mPartPriorities[i] = 0; } + updateNpcBase(); + if (!disableListener) mPtr.getClass().getInventoryStore(mPtr).setListener(this, mPtr); - - updateNpcBase(); } void NpcAnimation::setViewMode(NpcAnimation::ViewMode viewMode) @@ -179,12 +263,27 @@ void NpcAnimation::updateNpcBase() } else { - if (isVampire) + mHeadModel = ""; + if (isVampire) // FIXME: fall back to regular head when getVampireHead fails? mHeadModel = getVampireHead(mNpc->mRace, mNpc->mFlags & ESM::NPC::Female); - else - mHeadModel = "meshes\\" + store.get().find(mNpc->mHead)->mModel; + else if (!mNpc->mHead.empty()) + { + const ESM::BodyPart* bp = store.get().search(mNpc->mHead); + if (bp) + mHeadModel = "meshes\\" + bp->mModel; + else + std::cerr << "Failed to load body part '" << mNpc->mHead << "'" << std::endl; + } - mHairModel = "meshes\\" + store.get().find(mNpc->mHair)->mModel; + mHairModel = ""; + if (!mNpc->mHair.empty()) + { + const ESM::BodyPart* bp = store.get().search(mNpc->mHair); + if (bp) + mHairModel = "meshes\\" + bp->mModel; + else + std::cerr << "Failed to load body part '" << mNpc->mHair << "'" << std::endl; + } } bool isBeast = (race->mData.mFlags & ESM::Race::Beast) != 0; @@ -195,6 +294,7 @@ void NpcAnimation::updateNpcBase() (!isWerewolf ? !isBeast ? "meshes\\base_anim.1st.nif" : "meshes\\base_animkna.1st.nif" : "meshes\\wolf\\skin.1st.nif"); + smodel = Misc::ResourceHelpers::correctActorModelPath(smodel); setObjectRoot(smodel, true); if(mViewMode != VM_FirstPerson) @@ -203,11 +303,11 @@ void NpcAnimation::updateNpcBase() if(!isWerewolf) { if(Misc::StringUtils::lowerCase(mNpc->mRace).find("argonian") != std::string::npos) - addAnimSource("meshes\\argonian_swimkna.nif"); + addAnimSource("meshes\\xargonian_swimkna.nif"); else if(!mNpc->isMale() && !isBeast) - addAnimSource("meshes\\base_anim_female.nif"); + addAnimSource("meshes\\xbase_anim_female.nif"); if(mNpc->mModel.length() > 0) - addAnimSource("meshes\\"+mNpc->mModel); + addAnimSource("meshes\\x"+mNpc->mModel); } } else @@ -219,11 +319,11 @@ void NpcAnimation::updateNpcBase() /* A bit counter-intuitive, but unlike third-person anims, it seems * beast races get both base_anim.1st.nif and base_animkna.1st.nif. */ - addAnimSource("meshes\\base_anim.1st.nif"); + addAnimSource("meshes\\xbase_anim.1st.nif"); if(isBeast) - addAnimSource("meshes\\base_animkna.1st.nif"); + addAnimSource("meshes\\xbase_animkna.1st.nif"); if(!mNpc->isMale() && !isBeast) - addAnimSource("meshes\\base_anim_female.1st.nif"); + addAnimSource("meshes\\xbase_anim_female.1st.nif"); } } @@ -235,13 +335,15 @@ void NpcAnimation::updateNpcBase() } void NpcAnimation::updateParts() -{ +{ + if (!mSkelBase) + return; + mAlpha = 1.f; const MWWorld::Class &cls = mPtr.getClass(); - MWWorld::InventoryStore &inv = cls.getInventoryStore(mPtr); NpcType curType = Type_Normal; - if (cls.getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Vampirism).mMagnitude > 0) + if (cls.getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Vampirism).getMagnitude() > 0) curType = Type_Vampire; if (cls.getNpcStats(mPtr).isWerewolf()) curType = Type_Werewolf; @@ -275,6 +377,9 @@ void NpcAnimation::updateParts() }; static const size_t slotlistsize = sizeof(slotlist)/sizeof(slotlist[0]); + bool wasArrowAttached = (mAmmunition.get()); + + MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); for(size_t i = 0;i < slotlistsize && mViewMode != VM_HeadOnly;i++) { MWWorld::ContainerStoreIterator store = inv.getSlot(slotlist[i].mSlot); @@ -324,9 +429,9 @@ void NpcAnimation::updateParts() if(mViewMode != VM_FirstPerson) { - if(mPartPriorities[ESM::PRT_Head] < 1) + if(mPartPriorities[ESM::PRT_Head] < 1 && !mHeadModel.empty()) addOrReplaceIndividualPart(ESM::PRT_Head, -1,1, mHeadModel); - if(mPartPriorities[ESM::PRT_Hair] < 1 && mPartPriorities[ESM::PRT_Head] <= 1) + if(mPartPriorities[ESM::PRT_Hair] < 1 && mPartPriorities[ESM::PRT_Head] <= 1 && !mHairModel.empty()) addOrReplaceIndividualPart(ESM::PRT_Hair, -1,1, mHairModel); } if(mViewMode == VM_HeadOnly) @@ -351,15 +456,18 @@ void NpcAnimation::updateParts() // Remember body parts so we only have to search through the store once for each race/gender/viewmode combination static std::map< std::pair,std::vector > sRaceMapping; - static const int Flag_Female = 1<<0; - static const int Flag_FirstPerson = 1<<1; - bool isWerewolf = (mNpcType == Type_Werewolf); int flags = (isWerewolf ? -1 : 0); if(!mNpc->isMale()) + { + static const int Flag_Female = 1<<0; flags |= Flag_Female; + } if(mViewMode == VM_FirstPerson) + { + static const int Flag_FirstPerson = 1<<1; flags |= Flag_FirstPerson; + } std::string race = (isWerewolf ? "werewolf" : Misc::StringUtils::lowerCase(mNpc->mRace)); std::pair thisCombination = std::make_pair(race, flags); @@ -465,6 +573,9 @@ void NpcAnimation::updateParts() "meshes\\"+bodypart->mModel); } } + + if (wasArrowAttached) + attachArrow(); } void NpcAnimation::addFirstPersonOffset(const Ogre::Vector3 &offset) @@ -484,9 +595,9 @@ public: } }; -NifOgre::ObjectScenePtr NpcAnimation::insertBoundedPart(const std::string &model, int group, const std::string &bonename, bool enchantedGlow, Ogre::Vector3* glowColor) +NifOgre::ObjectScenePtr NpcAnimation::insertBoundedPart(const std::string &model, int group, const std::string &bonename, const std::string &bonefilter, bool enchantedGlow, Ogre::Vector3* glowColor) { - NifOgre::ObjectScenePtr objects = NifOgre::Loader::createObjects(mSkelBase, bonename, mInsert, model); + NifOgre::ObjectScenePtr objects = NifOgre::Loader::createObjects(mSkelBase, bonename, bonefilter, mInsert, model); setRenderProperties(objects, (mViewMode == VM_FirstPerson) ? RV_FirstPerson : mVisibilityFlags, RQG_Main, RQG_Alpha, 0, enchantedGlow, glowColor); @@ -513,24 +624,33 @@ NifOgre::ObjectScenePtr NpcAnimation::insertBoundedPart(const std::string &model } Ogre::Vector3 NpcAnimation::runAnimation(float timepassed) -{ +{ Ogre::Vector3 ret = Animation::runAnimation(timepassed); - Ogre::SkeletonInstance *baseinst = mSkelBase->getSkeleton(); - if(mViewMode == VM_FirstPerson) - { - float pitch = mPtr.getRefData().getPosition().rot[0]; - Ogre::Node *node = baseinst->getBone("Bip01 Neck"); - node->pitch(Ogre::Radian(-pitch), Ogre::Node::TS_WORLD); + mHeadAnimationTime->update(timepassed); - // This has to be done before this function ends; - // updateSkeletonInstance, below, touches the hands. - node->translate(mFirstPersonOffset, Ogre::Node::TS_WORLD); - } - else + if (mSkelBase) { - // In third person mode we may still need pitch for ranged weapon targeting - pitchSkeleton(mPtr.getRefData().getPosition().rot[0], baseinst); + Ogre::SkeletonInstance *baseinst = mSkelBase->getSkeleton(); + if(mViewMode == VM_FirstPerson) + { + float pitch = mPtr.getRefData().getPosition().rot[0]; + Ogre::Node *node = baseinst->getBone("Bip01 Neck"); + node->pitch(Ogre::Radian(-pitch), Ogre::Node::TS_WORLD); + + // This has to be done before this function ends; + // updateSkeletonInstance, below, touches the hands. + node->translate(mFirstPersonOffset, Ogre::Node::TS_WORLD); + } + else + { + // In third person mode we may still need pitch for ranged weapon targeting + pitchSkeleton(mPtr.getRefData().getPosition().rot[0], baseinst); + + Ogre::Node* node = baseinst->getBone("Bip01 Head"); + if (node) + node->rotate(Ogre::Quaternion(mHeadYaw, Ogre::Vector3::UNIT_Z) * Ogre::Quaternion(mHeadPitch, Ogre::Vector3::UNIT_X), Ogre::Node::TS_WORLD); + } } mFirstPersonOffset = 0.f; // reset the X, Y, Z offset for the next frame. @@ -539,13 +659,16 @@ Ogre::Vector3 NpcAnimation::runAnimation(float timepassed) if (mObjectParts[i].isNull()) continue; std::vector >::iterator ctrl(mObjectParts[i]->mControllers.begin()); - for(;ctrl != mObjectParts[i]->mControllers.end();ctrl++) + for(;ctrl != mObjectParts[i]->mControllers.end();++ctrl) ctrl->update(); - Ogre::Entity *ent = mObjectParts[i]->mSkelBase; - if(!ent) continue; - updateSkeletonInstance(baseinst, ent->getSkeleton()); - ent->getAllAnimationStates()->_notifyDirty(); + if (!isSkinned(mObjectParts[i])) + continue; + + if (mSkelBase) + updateSkeletonInstance(mSkelBase->getSkeleton(), mObjectParts[i]->mSkelBase->getSkeleton()); + + mObjectParts[i]->mSkelBase->getAllAnimationStates()->_notifyDirty(); } return ret; @@ -557,6 +680,11 @@ void NpcAnimation::removeIndividualPart(ESM::PartReferenceType type) mPartslots[type] = -1; mObjectParts[type].setNull(); + if (!mSoundIds[type].empty() && !mSoundsDisabled) + { + MWBase::Environment::get().getSoundManager()->stopSound3D(mPtr, mSoundIds[type]); + mSoundIds[type].clear(); + } } void NpcAnimation::reserveIndividualPart(ESM::PartReferenceType type, int group, int priority) @@ -586,8 +714,33 @@ bool NpcAnimation::addOrReplaceIndividualPart(ESM::PartReferenceType type, int g removeIndividualPart(type); mPartslots[type] = group; mPartPriorities[type] = priority; + try + { + const std::string& bonename = sPartList.at(type); + // PRT_Hair seems to be the only type that breaks consistency and uses a filter that's different from the attachment bone + const std::string bonefilter = (type == ESM::PRT_Hair) ? "hair" : bonename; + mObjectParts[type] = insertBoundedPart(mesh, group, bonename, bonefilter, enchantedGlow, glowColor); + } + catch (std::exception& e) + { + std::cerr << "Error adding NPC part: " << e.what() << std::endl; + return false; + } - mObjectParts[type] = insertBoundedPart(mesh, group, sPartList.at(type), enchantedGlow, glowColor); + if (!mSoundsDisabled) + { + MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); + MWWorld::ContainerStoreIterator csi = inv.getSlot(group < 0 ? MWWorld::InventoryStore::Slot_Helmet : group); + if (csi != inv.end()) + { + mSoundIds[type] = csi->getClass().getSound(*csi); + if (!mSoundIds[type].empty()) + { + MWBase::Environment::get().getSoundManager()->playSound3D(mPtr, mSoundIds[type], 1.0f, 1.0f, MWBase::SoundManager::Play_TypeSfx, + MWBase::SoundManager::Play_Loop); + } + } + } if(mObjectParts[type]->mSkelBase) { Ogre::SkeletonInstance *skel = mObjectParts[type]->mSkelBase->getSkeleton(); @@ -609,18 +762,33 @@ bool NpcAnimation::addOrReplaceIndividualPart(ESM::PartReferenceType type, int g } } - updateSkeletonInstance(mSkelBase->getSkeleton(), skel); + if (isSkinned(mObjectParts[type])) + updateSkeletonInstance(mSkelBase->getSkeleton(), skel); } std::vector >::iterator ctrl(mObjectParts[type]->mControllers.begin()); - for(;ctrl != mObjectParts[type]->mControllers.end();ctrl++) + for(;ctrl != mObjectParts[type]->mControllers.end();++ctrl) { if(ctrl->getSource().isNull()) { ctrl->setSource(mNullAnimationTimePtr); if (type == ESM::PRT_Head) + { ctrl->setSource(mHeadAnimationTime); + const NifOgre::TextKeyMap& keys = mObjectParts[type]->mTextKeys; + for (NifOgre::TextKeyMap::const_iterator it = keys.begin(); it != keys.end(); ++it) + { + if (Misc::StringUtils::ciEqual(it->second, "talk: start")) + mHeadAnimationTime->setTalkStart(it->first); + if (Misc::StringUtils::ciEqual(it->second, "talk: stop")) + mHeadAnimationTime->setTalkStop(it->first); + if (Misc::StringUtils::ciEqual(it->second, "blink: start")) + mHeadAnimationTime->setBlinkStart(it->first); + if (Misc::StringUtils::ciEqual(it->second, "blink: stop")) + mHeadAnimationTime->setBlinkStop(it->first); + } + } else if (type == ESM::PRT_Weapon) ctrl->setSource(mWeaponAnimationTime); } @@ -636,7 +804,7 @@ void NpcAnimation::addPartGroup(int group, int priority, const std::vector::const_iterator part(parts.begin()); - for(;part != parts.end();part++) + for(;part != parts.end();++part) { const ESM::BodyPart *bodypart = 0; if(!mNpc->isMale() && !part->mFemale.empty()) @@ -651,6 +819,8 @@ void NpcAnimation::addPartGroup(int group, int priority, const std::vectormData.mPart == ESM::BodyPart::MP_Upperarm)) bodypart = NULL; } + else if (!bodypart) + std::cerr << "Failed to find body part '" << part->mFemale << "'" << std::endl; } if(!bodypart && !part->mMale.empty()) { @@ -664,6 +834,8 @@ void NpcAnimation::addPartGroup(int group, int priority, const std::vectormData.mPart == ESM::BodyPart::MP_Upperarm)) bodypart = NULL; } + else if (!bodypart) + std::cerr << "Failed to find body part '" << part->mMale << "'" << std::endl; } if(bodypart) @@ -678,7 +850,7 @@ void NpcAnimation::showWeapons(bool showWeapon) mShowWeapons = showWeapon; if(showWeapon) { - MWWorld::InventoryStore &inv = mPtr.getClass().getInventoryStore(mPtr); + MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); MWWorld::ContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); if(weapon != inv.end()) { @@ -711,9 +883,8 @@ void NpcAnimation::showWeapons(bool showWeapon) void NpcAnimation::showCarriedLeft(bool show) { mShowCarriedLeft = show; - MWWorld::InventoryStore &inv = mPtr.getClass().getInventoryStore(mPtr); + MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); MWWorld::ContainerStoreIterator iter = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); - if(show && iter != inv.end()) { Ogre::Vector3 glowColor = getEnchantmentColor(*iter); @@ -797,6 +968,11 @@ void NpcAnimation::setAlpha(float alpha) } } +void NpcAnimation::enableHeadAnimation(bool enable) +{ + mHeadAnimationTime->setEnabled(enable); +} + void NpcAnimation::preRender(Ogre::Camera *camera) { Animation::preRender(camera); @@ -841,4 +1017,42 @@ void NpcAnimation::applyAlpha(float alpha, Ogre::Entity *ent, NifOgre::ObjectSce } } +void NpcAnimation::equipmentChanged() +{ + updateParts(); +} + +void NpcAnimation::setVampire(bool vampire) +{ + if (mNpcType == Type_Werewolf) // we can't have werewolf vampires, can we + return; + if ((mNpcType == Type_Vampire) != vampire) + { + if (mPtr == MWBase::Environment::get().getWorld()->getPlayerPtr()) + MWBase::Environment::get().getWorld()->reattachPlayerCamera(); + else + rebuild(); + } +} + +void NpcAnimation::setHeadPitch(Ogre::Radian pitch) +{ + mHeadPitch = pitch; +} + +void NpcAnimation::setHeadYaw(Ogre::Radian yaw) +{ + mHeadYaw = yaw; +} + +Ogre::Radian NpcAnimation::getHeadPitch() const +{ + return mHeadPitch; +} + +Ogre::Radian NpcAnimation::getHeadYaw() const +{ + return mHeadYaw; +} + } diff --git a/apps/openmw/mwrender/npcanimation.hpp b/apps/openmw/mwrender/npcanimation.hpp index 8ec46facd..90b1c269b 100644 --- a/apps/openmw/mwrender/npcanimation.hpp +++ b/apps/openmw/mwrender/npcanimation.hpp @@ -19,8 +19,29 @@ class HeadAnimationTime : public Ogre::ControllerValue { private: MWWorld::Ptr mReference; + float mTalkStart; + float mTalkStop; + float mBlinkStart; + float mBlinkStop; + + float mBlinkTimer; + + bool mEnabled; + + float mValue; +private: + void resetBlinkTimer(); public: - HeadAnimationTime(MWWorld::Ptr reference) : mReference(reference) {} + HeadAnimationTime(MWWorld::Ptr reference); + + void update(float dt); + + void setEnabled(bool enabled); + + void setTalkStart(float value); + void setTalkStop(float value); + void setBlinkStart(float value); + void setBlinkStop(float value); virtual Ogre::Real getValue() const; virtual void setValue(Ogre::Real value) @@ -30,7 +51,7 @@ public: class NpcAnimation : public Animation, public WeaponAnimation, public MWWorld::InventoryStoreListener { public: - virtual void equipmentChanged() { updateParts(); } + virtual void equipmentChanged(); virtual void permanentEffectAdded(const ESM::MagicEffect *magicEffect, bool isNew, bool playSound); public: @@ -49,6 +70,7 @@ private: // Bounded Parts NifOgre::ObjectScenePtr mObjectParts[ESM::PRT_Count]; + std::string mSoundIds[ESM::PRT_Count]; const ESM::NPC *mNpc; std::string mHeadModel; @@ -76,10 +98,15 @@ private: Ogre::SharedPtr mWeaponAnimationTime; float mAlpha; + bool mSoundsDisabled; + + Ogre::Radian mHeadYaw; + Ogre::Radian mHeadPitch; void updateNpcBase(); NifOgre::ObjectScenePtr insertBoundedPart(const std::string &model, int group, const std::string &bonename, + const std::string &bonefilter, bool enchantedGlow, Ogre::Vector3* glowColor=NULL); void removeIndividualPart(ESM::PartReferenceType type); @@ -102,12 +129,15 @@ public: * one listener at a time, so you shouldn't do this if creating several NpcAnimations * for the same Ptr, eg preview dolls for the player. * Those need to be manually rendered anyway. + * @param disableSounds Same as \a disableListener but for playing items sounds * @param viewMode */ NpcAnimation(const MWWorld::Ptr& ptr, Ogre::SceneNode* node, int visibilityFlags, bool disableListener = false, - ViewMode viewMode=VM_Normal); + bool disableSounds = false, ViewMode viewMode=VM_Normal); virtual ~NpcAnimation(); + virtual void enableHeadAnimation(bool enable); + virtual void setWeaponGroup(const std::string& group) { mWeaponAnimationTime->setGroup(group); } virtual Ogre::Vector3 runAnimation(float timepassed); @@ -116,8 +146,13 @@ public: /// to indicate the facing orientation of the character. virtual void setPitchFactor(float factor) { mPitchFactor = factor; } + virtual void setHeadPitch(Ogre::Radian pitch); + virtual void setHeadYaw(Ogre::Radian yaw); + virtual Ogre::Radian getHeadPitch() const; + virtual Ogre::Radian getHeadYaw() const; + virtual void showWeapons(bool showWeapon); - virtual void showCarriedLeft(bool showa); + virtual void showCarriedLeft(bool show); virtual void attachArrow(); virtual void releaseArrow(); @@ -142,6 +177,8 @@ public: /// Make the NPC only partially visible virtual void setAlpha(float alpha); + virtual void setVampire(bool vampire); + /// Prepare this animation for being rendered with \a camera (rotates billboard nodes) virtual void preRender (Ogre::Camera* camera); }; diff --git a/apps/openmw/mwrender/objects.cpp b/apps/openmw/mwrender/objects.cpp index d9e20e1f8..965083019 100644 --- a/apps/openmw/mwrender/objects.cpp +++ b/apps/openmw/mwrender/objects.cpp @@ -79,9 +79,6 @@ void Objects::insertModel(const MWWorld::Ptr &ptr, const std::string &mesh, bool std::auto_ptr anim(new ObjectAnimation(ptr, mesh)); - if(ptr.getTypeName() == typeid(ESM::Light).name()) - anim->addLight(ptr.get()->mBase); - if (!mesh.empty()) { Ogre::AxisAlignedBox bounds = anim->getWorldBounds(); @@ -240,20 +237,6 @@ Ogre::AxisAlignedBox Objects::getDimensions(MWWorld::CellStore* cell) return mBounds[cell]; } -void Objects::enableLights() -{ - PtrAnimationMap::const_iterator it = mObjects.begin(); - for(;it != mObjects.end();++it) - it->second->enableLights(true); -} - -void Objects::disableLights() -{ - PtrAnimationMap::const_iterator it = mObjects.begin(); - for(;it != mObjects.end();++it) - it->second->enableLights(false); -} - void Objects::update(float dt, Ogre::Camera* camera) { PtrAnimationMap::const_iterator it = mObjects.begin(); diff --git a/apps/openmw/mwrender/objects.hpp b/apps/openmw/mwrender/objects.hpp index 02e974e2d..adfe5ca26 100644 --- a/apps/openmw/mwrender/objects.hpp +++ b/apps/openmw/mwrender/objects.hpp @@ -45,9 +45,6 @@ public: ObjectAnimation* getAnimation(const MWWorld::Ptr &ptr); - void enableLights(); - void disableLights(); - void update (float dt, Ogre::Camera* camera); ///< per-frame update diff --git a/apps/openmw/mwrender/occlusionquery.cpp b/apps/openmw/mwrender/occlusionquery.cpp index 92a49acc0..2693d68b2 100644 --- a/apps/openmw/mwrender/occlusionquery.cpp +++ b/apps/openmw/mwrender/occlusionquery.cpp @@ -19,14 +19,14 @@ using namespace Ogre; OcclusionQuery::OcclusionQuery(OEngine::Render::OgreRenderer* renderer, SceneNode* sunNode) : mSunTotalAreaQuery(0), mSunVisibleAreaQuery(0), mActiveQuery(0), - mDoQuery(0), mSunVisibility(0), + mBBQueryVisible(0), mBBQueryTotal(0), mSunNode(sunNode), mBBNodeReal(0), + mSunVisibility(0), mWasVisible(false), mActive(false), - mFirstFrame(true) + mFirstFrame(true), + mDoQuery(0), + mRendering(renderer) { - mRendering = renderer; - mSunNode = sunNode; - try { RenderSystem* renderSystem = Root::getSingleton().getRenderSystem(); diff --git a/apps/openmw/mwrender/refraction.cpp b/apps/openmw/mwrender/refraction.cpp index 7d728b721..6cc49089a 100644 --- a/apps/openmw/mwrender/refraction.cpp +++ b/apps/openmw/mwrender/refraction.cpp @@ -33,7 +33,7 @@ namespace MWRender Ogre::Viewport* vp = mRenderTarget->addViewport(mCamera); vp->setOverlaysEnabled(false); vp->setShadowsEnabled(false); - vp->setVisibilityMask(RV_Actors + RV_Misc + RV_Statics + RV_StaticsSmall + RV_Terrain + RV_Sky + RV_FirstPerson); + vp->setVisibilityMask(RV_Refraction); vp->setMaterialScheme("water_refraction"); vp->setBackgroundColour (Ogre::ColourValue(0.090195, 0.115685, 0.12745)); mRenderTarget->setAutoUpdated(true); @@ -50,7 +50,8 @@ namespace MWRender void Refraction::preRenderTargetUpdate(const Ogre::RenderTargetEvent& evt) { - mParentCamera->getParentSceneNode ()->needUpdate (); + if (mParentCamera->isAttached()) + mParentCamera->getParentSceneNode ()->needUpdate (); mCamera->setOrientation(mParentCamera->getDerivedOrientation()); mCamera->setPosition(mParentCamera->getDerivedPosition()); mCamera->setNearClipDistance(mParentCamera->getNearClipDistance()); diff --git a/apps/openmw/mwrender/renderconst.hpp b/apps/openmw/mwrender/renderconst.hpp index 44599ebee..cfd84cb32 100644 --- a/apps/openmw/mwrender/renderconst.hpp +++ b/apps/openmw/mwrender/renderconst.hpp @@ -21,6 +21,7 @@ enum RenderQueueGroups RQG_UnderWater = Ogre::RENDER_QUEUE_4, RQG_Water = RQG_Alpha, + RQG_Ripples = RQG_Water+1, // Sky late (sun & sun flare) RQG_SkiesLate = Ogre::RENDER_QUEUE_SKIES_LATE @@ -30,39 +31,44 @@ enum RenderQueueGroups enum VisibilityFlags { // Terrain - RV_Terrain = 1, + RV_Terrain = (1<<0), // Statics (e.g. trees, houses) - RV_Statics = 2, + RV_Statics = (1<<1), // Small statics - RV_StaticsSmall = 4, + RV_StaticsSmall = (1<<2), // Water - RV_Water = 8, + RV_Water = (1<<3), // Actors (npcs, creatures) - RV_Actors = 16, + RV_Actors = (1<<4), // Misc objects (containers, dynamic objects) - RV_Misc = 32, + RV_Misc = (1<<5), - RV_Sky = 64, + // VFX, don't appear on map and don't cast shadows + RV_Effects = (1<<6), + + RV_Sky = (1<<7), // not visible in reflection - RV_NoReflection = 128, + RV_NoReflection = (1<<8), - RV_OcclusionQuery = 256, + RV_OcclusionQuery = (1<<9), - RV_Debug = 512, + RV_Debug = (1<<10), // overlays, we only want these on the main render target - RV_Overlay = 1024, + RV_Overlay = (1<<11), // First person meshes do not cast shadows - RV_FirstPerson = 2048, + RV_FirstPerson = (1<<12), + + RV_Map = RV_Terrain + RV_Statics + RV_StaticsSmall + RV_Misc + RV_Water, - RV_Map = RV_Terrain + RV_Statics + RV_StaticsSmall + RV_Misc + RV_Water + RV_Refraction = RV_Actors + RV_Misc + RV_Statics + RV_StaticsSmall + RV_Terrain + RV_Effects + RV_Sky + RV_FirstPerson }; } diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index 23edb3a7f..05b43d54f 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -23,6 +23,7 @@ #include #include +#include #include "../mwworld/esmstore.hpp" #include "../mwworld/class.hpp" @@ -35,6 +36,7 @@ #include "../mwbase/statemanager.hpp" #include "../mwmechanics/creaturestats.hpp" +#include "../mwmechanics/npcstats.hpp" #include "../mwworld/ptr.hpp" @@ -45,7 +47,6 @@ #include "globalmap.hpp" #include "terrainstorage.hpp" #include "effectmanager.hpp" -#include "terraingrid.hpp" using namespace MWRender; using namespace Ogre; @@ -63,6 +64,7 @@ RenderingManager::RenderingManager(OEngine::Render::OgreRenderer& _rend, const b , mPhysicsEngine(engine) , mTerrain(NULL) , mEffectManager(NULL) + , mRenderWorld(true) { mActors = new MWRender::Actors(mRendering, this); mObjects = new MWRender::Objects(mRendering); @@ -113,17 +115,12 @@ RenderingManager::RenderingManager(OEngine::Render::OgreRenderer& _rend, const b mFactory->loadAllFiles(); - // Compressed textures with 0 mip maps are bugged in 1.8, so disable mipmap generator in that case - // ( https://ogre3d.atlassian.net/browse/OGRE-259 ) -#if OGRE_VERSION >= (1 << 16 | 9 << 8 | 0) TextureManager::getSingleton().setDefaultNumMipmaps(Settings::Manager::getInt("num mipmaps", "General")); -#else - TextureManager::getSingleton().setDefaultNumMipmaps(0); -#endif // Set default texture filtering options TextureFilterOptions tfo; std::string filter = Settings::Manager::getString("texture filtering", "General"); + if (filter == "anisotropic") tfo = TFO_ANISOTROPIC; else if (filter == "trilinear") tfo = TFO_TRILINEAR; else if (filter == "bilinear") tfo = TFO_BILINEAR; @@ -176,7 +173,7 @@ RenderingManager::RenderingManager(OEngine::Render::OgreRenderer& _rend, const b mDebugging = new Debugging(mRootNode, engine); mLocalMap = new MWRender::LocalMap(&mRendering, this); - mWater = new MWRender::Water(mRendering.getCamera(), this); + mWater = new MWRender::Water(mRendering.getCamera(), this, mFallback); setMenuTransparency(Settings::Manager::getFloat("menu transparency", "GUI")); } @@ -212,11 +209,6 @@ MWRender::Actors& RenderingManager::getActors(){ return *mActors; } -OEngine::Render::Fader* RenderingManager::getFader() -{ - return mRendering.getFader(); -} - MWRender::Camera* RenderingManager::getCamera() const { return mCamera; @@ -243,6 +235,15 @@ bool RenderingManager::toggleWater() return mWater->toggle(); } +bool RenderingManager::toggleWorld() +{ + mRenderWorld = !mRenderWorld; + + int visibilityMask = mRenderWorld ? ~int(0) : 0; + mRendering.getViewport()->setVisibilityMask(visibilityMask); + return mRenderWorld; +} + void RenderingManager::cellAdded (MWWorld::CellStore *store) { if (store->isExterior()) @@ -251,13 +252,12 @@ void RenderingManager::cellAdded (MWWorld::CellStore *store) mObjects->buildStaticGeometry (*store); sh::Factory::getInstance().unloadUnreferencedMaterials(); mDebugging->cellAdded(store); - waterAdded(store); } -void RenderingManager::addObject (const MWWorld::Ptr& ptr){ +void RenderingManager::addObject (const MWWorld::Ptr& ptr, const std::string& model){ const MWWorld::Class& class_ = ptr.getClass(); - class_.insertObjectRendering(ptr, *this); + class_.insertObjectRendering(ptr, model, *this); } void RenderingManager::removeObject (const MWWorld::Ptr& ptr) @@ -295,6 +295,8 @@ void RenderingManager::rotateObject(const MWWorld::Ptr &ptr) void RenderingManager::updateObjectCell(const MWWorld::Ptr &old, const MWWorld::Ptr &cur) { + if (!old.getRefData().getBaseNode()) + return; Ogre::SceneNode *child = mRendering.getScene()->getSceneNode(old.getRefData().getHandle()); @@ -313,7 +315,7 @@ void RenderingManager::updatePlayerPtr(const MWWorld::Ptr &ptr) if(mPlayerAnimation) mPlayerAnimation->updatePtr(ptr); if(mCamera->getHandle() == ptr.getRefData().getHandle()) - mCamera->attachTo(ptr); + attachCameraTo(ptr); } void RenderingManager::rebuildPtr(const MWWorld::Ptr &ptr) @@ -328,7 +330,7 @@ void RenderingManager::rebuildPtr(const MWWorld::Ptr &ptr) anim->rebuild(); if(mCamera->getHandle() == ptr.getRefData().getHandle()) { - mCamera->attachTo(ptr); + attachCameraTo(ptr); mCamera->setAnimation(anim); } } @@ -344,10 +346,13 @@ void RenderingManager::update (float duration, bool paused) MWWorld::Ptr player = world->getPlayerPtr(); - int blind = player.getClass().getCreatureStats(player).getMagicEffects().get(ESM::MagicEffect::Blind).mMagnitude; - mRendering.getFader()->setFactor(std::max(0.f, 1.f-(blind / 100.f))); + int blind = player.getClass().getCreatureStats(player).getMagicEffects().get(ESM::MagicEffect::Blind).getMagnitude(); + MWBase::Environment::get().getWindowManager()->setBlindness(std::max(0, std::min(100, blind))); setAmbientMode(); + if (player.getClass().getNpcStats(player).isWerewolf()) + MWBase::Environment::get().getWindowManager()->setWerewolfOverlay(mCamera->isFirstPerson()); + // player position MWWorld::RefData &data = player.getRefData(); Ogre::Vector3 playerPos(data.getPosition().pos); @@ -404,6 +409,7 @@ void RenderingManager::update (float duration, bool paused) mSkyManager->setGlare(mOcclusionQuery->getSunVisibility()); + mWater->changeCell(player.getCell()->getCell()); mWater->updateUnderwater(world->isUnderwater(player.getCell(), cam)); @@ -422,18 +428,12 @@ void RenderingManager::postRenderTargetUpdate(const RenderTargetEvent &evt) mOcclusionQuery->setActive(false); } -void RenderingManager::waterAdded (MWWorld::CellStore *store) +void RenderingManager::setWaterEnabled(bool enable) { - if (store->getCell()->mData.mFlags & ESM::Cell::HasWater) - { - mWater->changeCell (store->getCell()); - mWater->setActive(true); - } - else - removeWater(); + mWater->setActive(enable); } -void RenderingManager::setWaterHeight(const float height) +void RenderingManager::setWaterHeight(float height) { mWater->setHeight(height); } @@ -500,7 +500,7 @@ bool RenderingManager::toggleRenderMode(int mode) } } -void RenderingManager::configureFog(MWWorld::CellStore &mCell) +void RenderingManager::configureFog(const MWWorld::CellStore &mCell) { Ogre::ColourValue color; color.setAsABGR (mCell.getCell()->mAmbi.mFog); @@ -511,12 +511,12 @@ void RenderingManager::configureFog(MWWorld::CellStore &mCell) void RenderingManager::configureFog(const float density, const Ogre::ColourValue& colour) { mFogColour = colour; - float max = Settings::Manager::getFloat("max viewing distance", "Viewing distance"); + float max = Settings::Manager::getFloat("viewing distance", "Viewing distance"); if (density == 0) { mFogStart = 0; - mFogEnd = std::numeric_limits().max(); + mFogEnd = std::numeric_limits::max(); mRendering.getCamera()->setFarClipDistance (max); } else @@ -585,24 +585,6 @@ void RenderingManager::configureAmbient(MWWorld::CellStore &mCell) sunEnable(false); } } -// Switch through lighting modes. - -void RenderingManager::toggleLight() -{ - if (mAmbientMode==2) - mAmbientMode = 0; - else - ++mAmbientMode; - - switch (mAmbientMode) - { - case 0: std::cout << "Setting lights to normal\n"; break; - case 1: std::cout << "Turning the lights up\n"; break; - case 2: std::cout << "Turning the lights to full\n"; break; - } - - setAmbientMode(); -} void RenderingManager::setSunColour(const Ogre::ColourValue& colour) { @@ -616,7 +598,7 @@ void RenderingManager::setAmbientColour(const Ogre::ColourValue& colour) mAmbientColor = colour; MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); - int nightEye = player.getClass().getCreatureStats(player).getMagicEffects().get(ESM::MagicEffect::NightEye).mMagnitude; + int nightEye = player.getClass().getCreatureStats(player).getMagicEffects().get(ESM::MagicEffect::NightEye).getMagnitude(); Ogre::ColourValue final = colour; final += Ogre::ColourValue(0.7,0.7,0.7,0) * std::min(1.f, (nightEye/100.f)); @@ -648,12 +630,12 @@ void RenderingManager::sunDisable(bool real) } } -void RenderingManager::setSunDirection(const Ogre::Vector3& direction, bool is_moon) +void RenderingManager::setSunDirection(const Ogre::Vector3& direction, bool is_night) { // direction * -1 (because 'direction' is camera to sun vector and not sun to camera), if (mSun) mSun->setDirection(Vector3(-direction.x, -direction.y, -direction.z)); - mSkyManager->setSunDirection(direction, is_moon); + mSkyManager->setSunDirection(direction, is_night); } void RenderingManager::setGlare(bool glare) @@ -696,24 +678,20 @@ void RenderingManager::writeFog(MWWorld::CellStore* cell) void RenderingManager::disableLights(bool sun) { - mObjects->disableLights(); + mActors->disableLights(); sunDisable(sun); } void RenderingManager::enableLights(bool sun) { - mObjects->enableLights(); + mActors->enableLights(); sunEnable(sun); } -Shadows* RenderingManager::getShadows() -{ - return mShadows; -} - void RenderingManager::notifyWorldSpaceChanged() { mEffectManager->clear(); + mWater->clearRipples(); } Ogre::Vector4 RenderingManager::boundingBoxToScreen(Ogre::AxisAlignedBox bounds) @@ -766,7 +744,7 @@ void RenderingManager::processChangedSettings(const Settings::CategorySettingVec { setMenuTransparency(Settings::Manager::getFloat("menu transparency", "GUI")); } - else if (it->second == "max viewing distance" && it->first == "Viewing distance") + else if (it->second == "viewing distance" && it->first == "Viewing distance") { if (!MWBase::Environment::get().getWorld()->isCellExterior() && !MWBase::Environment::get().getWorld()->isCellQuasiExterior() && MWBase::Environment::get().getWorld()->getPlayerPtr().mCell) @@ -777,15 +755,14 @@ void RenderingManager::processChangedSettings(const Settings::CategorySettingVec || it->second == "resolution y" || it->second == "fullscreen")) changeRes = true; - else if (it->first == "Video" && it->second == "vsync") - { - // setVSyncEnabled is bugged in 1.8 -#if OGRE_VERSION >= (1 << 16 | 9 << 8 | 0) - mRendering.getWindow()->setVSyncEnabled(Settings::Manager::getBool("vsync", "Video")); -#endif - } + else if (it->first == "Video" && it->second == "window border") + changeRes = true; else if (it->second == "field of view" && it->first == "General") mRendering.setFov(Settings::Manager::getFloat("field of view", "General")); + else if (it->second == "gamma" && it->first == "General") + { + mRendering.setWindowGammaContrast(Settings::Manager::getFloat("gamma", "General"), Settings::Manager::getFloat("contrast", "General")); + } else if ((it->second == "texture filtering" && it->first == "General") || (it->second == "anisotropy" && it->first == "General")) { @@ -841,6 +818,7 @@ void RenderingManager::processChangedSettings(const Settings::CategorySettingVec unsigned int x = Settings::Manager::getInt("resolution x", "Video"); unsigned int y = Settings::Manager::getInt("resolution y", "Video"); bool fullscreen = Settings::Manager::getBool("fullscreen", "Video"); + bool windowBorder = Settings::Manager::getBool("window border", "Video"); SDL_Window* window = mRendering.getSDLWindow(); @@ -859,7 +837,10 @@ void RenderingManager::processChangedSettings(const Settings::CategorySettingVec SDL_SetWindowFullscreen(window, fullscreen); } else + { SDL_SetWindowSize(window, x, y); + SDL_SetWindowBordered(window, windowBorder ? SDL_TRUE : SDL_FALSE); + } } mWater->processChangedSettings(settings); @@ -875,10 +856,9 @@ void RenderingManager::processChangedSettings(const Settings::CategorySettingVec void RenderingManager::setMenuTransparency(float val) { - Ogre::TexturePtr tex = Ogre::TextureManager::getSingleton().getByName("transparent.png"); - std::vector buffer; + Ogre::TexturePtr tex = Ogre::TextureManager::getSingleton().getByName("transparent.png"); std::vector buffer; buffer.resize(1); - buffer[0] = (int(255*val) << 24); + buffer[0] = (int(255*val) << 24) | (255 << 16) | (255 << 8) | 255; memcpy(tex->getBuffer()->lock(Ogre::HardwareBuffer::HBL_DISCARD), &buffer[0], 1*4); tex->getBuffer()->unlock(); } @@ -901,7 +881,13 @@ void RenderingManager::getTriangleBatchCount(unsigned int &triangles, unsigned i void RenderingManager::setupPlayer(const MWWorld::Ptr &ptr) { ptr.getRefData().setBaseNode(mRendering.getScene()->getSceneNode("player")); - mCamera->attachTo(ptr); + attachCameraTo(ptr); +} + +void RenderingManager::attachCameraTo(const MWWorld::Ptr &ptr) +{ + Ogre::SceneNode* cameraNode = mCamera->attachTo(ptr); + mSkyManager->attachToNode(cameraNode); } void RenderingManager::renderPlayer(const MWWorld::Ptr &ptr) @@ -950,9 +936,14 @@ void RenderingManager::setCameraDistance(float dist, bool adjust, bool override) } } -void RenderingManager::getInteriorMapPosition (Ogre::Vector2 position, float& nX, float& nY, int &x, int& y) +void RenderingManager::worldToInteriorMapPosition (Ogre::Vector2 position, float& nX, float& nY, int &x, int& y) +{ + return mLocalMap->worldToInteriorMapPosition (position, nX, nY, x, y); +} + +Ogre::Vector2 RenderingManager::interiorMapToWorldPosition(float nX, float nY, int x, int y) { - return mLocalMap->getInteriorMapPosition (position, nX, nY, x, y); + return mLocalMap->interiorMapToWorldPosition(nX, nY, x, y); } bool RenderingManager::isPositionExplored (float nX, float nY, int x, int y, bool interior) @@ -1047,10 +1038,10 @@ void RenderingManager::enableTerrain(bool enable) if (!mTerrain) { if (Settings::Manager::getBool("distant land", "Terrain")) - mTerrain = new Terrain::DefaultWorld(mRendering.getScene(), new MWRender::TerrainStorage(), RV_Terrain, + mTerrain = new Terrain::DefaultWorld(mRendering.getScene(), new MWRender::TerrainStorage(true), RV_Terrain, Settings::Manager::getBool("shader", "Terrain"), Terrain::Align_XY, 1, 64); else - mTerrain = new MWRender::TerrainGrid(mRendering.getScene(), new MWRender::TerrainStorage(), RV_Terrain, + mTerrain = new Terrain::TerrainGrid(mRendering.getScene(), new MWRender::TerrainStorage(false), RV_Terrain, Settings::Manager::getBool("shader", "Terrain"), Terrain::Align_XY); mTerrain->applyMaterials(Settings::Manager::getBool("enabled", "Shadows"), Settings::Manager::getBool("split", "Shadows")); diff --git a/apps/openmw/mwrender/renderingmanager.hpp b/apps/openmw/mwrender/renderingmanager.hpp index ea7905cf5..9f029c1b9 100644 --- a/apps/openmw/mwrender/renderingmanager.hpp +++ b/apps/openmw/mwrender/renderingmanager.hpp @@ -4,8 +4,6 @@ #include "sky.hpp" #include "debugging.hpp" -#include - #include #include @@ -95,17 +93,13 @@ public: MWRender::Camera* getCamera() const; - void toggleLight(); bool toggleRenderMode(int mode); - OEngine::Render::Fader* getFader(); - void removeCell (MWWorld::CellStore *store); /// \todo this function should be removed later. Instead the rendering subsystems should track /// when rebatching is needed and update automatically at the end of each frame. void cellAdded (MWWorld::CellStore *store); - void waterAdded(MWWorld::CellStore *store); /// Clear all savegame-specific data (i.e. fog of war textures) void clear(); @@ -117,7 +111,7 @@ public: /// Write current fog of war for this cell to the CellStore void writeFog (MWWorld::CellStore* store); - void addObject (const MWWorld::Ptr& ptr); + void addObject (const MWWorld::Ptr& ptr, const std::string& model); void removeObject (const MWWorld::Ptr& ptr); void moveObject (const MWWorld::Ptr& ptr, const Ogre::Vector3& position); @@ -126,8 +120,10 @@ public: /// Updates an object's rotation void rotateObject (const MWWorld::Ptr& ptr); - void setWaterHeight(const float height); + void setWaterHeight(float height); + void setWaterEnabled(bool enabled); bool toggleWater(); + bool toggleWorld(); /// Updates object rendering after cell change /// \param old Object reference in previous cell @@ -145,7 +141,7 @@ public: void setAmbientColour(const Ogre::ColourValue& colour); void setSunColour(const Ogre::ColourValue& colour); - void setSunDirection(const Ogre::Vector3& direction, bool is_moon); + void setSunDirection(const Ogre::Vector3& direction, bool is_night); void sunEnable(bool real); ///< @param real whether or not to really disable the sunlight (otherwise just set diffuse to 0) void sunDisable(bool real); @@ -161,8 +157,6 @@ public: float getTerrainHeightAt (Ogre::Vector3 worldPos); - Shadows* getShadows(); - void notifyWorldSpaceChanged(); void getTriangleBatchCount(unsigned int &triangles, unsigned int &batches); @@ -189,7 +183,7 @@ public: ///< request the local map for a cell /// configure fog according to cell - void configureFog(MWWorld::CellStore &mCell); + void configureFog(const MWWorld::CellStore &mCell); /// configure fog manually void configureFog(const float density, const Ogre::ColourValue& colour); @@ -202,8 +196,11 @@ public: Ogre::Viewport* getViewport() { return mRendering.getViewport(); } - void getInteriorMapPosition (Ogre::Vector2 position, float& nX, float& nY, int &x, int& y); - ///< see MWRender::LocalMap::getInteriorMapPosition + void worldToInteriorMapPosition (Ogre::Vector2 position, float& nX, float& nY, int &x, int& y); + ///< see MWRender::LocalMap::worldToInteriorMapPosition + + Ogre::Vector2 interiorMapToWorldPosition (float nX, float nY, int x, int y); + ///< see MWRender::LocalMap::interiorMapToWorldPosition bool isPositionExplored (float nX, float nY, int x, int y, bool interior); ///< see MWRender::LocalMap::isPositionExplored @@ -224,6 +221,8 @@ private: void setAmbientMode(); void applyFog(bool underwater); + void attachCameraTo(const MWWorld::Ptr& ptr); + void setMenuTransparency(float val); bool mSunEnabled; @@ -270,6 +269,8 @@ private: MWRender::LocalMap* mLocalMap; MWRender::Shadows* mShadows; + + bool mRenderWorld; }; } diff --git a/apps/openmw/mwrender/ripplesimulation.cpp b/apps/openmw/mwrender/ripplesimulation.cpp index 64b5e48c3..bb1bccc0a 100644 --- a/apps/openmw/mwrender/ripplesimulation.cpp +++ b/apps/openmw/mwrender/ripplesimulation.cpp @@ -1,162 +1,72 @@ #include "ripplesimulation.hpp" -#include -#include -#include -#include -#include +#include + #include -#include -#include +#include +#include +#include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" -namespace MWRender -{ +#include "../mwworld/fallback.hpp" +#include "renderconst.hpp" -RippleSimulation::RippleSimulation(Ogre::SceneManager* mainSceneManager) - : mMainSceneMgr(mainSceneManager), - mTime(0), - mCurrentFrameOffset(0,0), - mPreviousFrameOffset(0,0), - mRippleCenter(0,0), - mTextureSize(512), - mRippleAreaLength(1000), - mImpulseSize(20), - mTexelOffset(0,0), - mFirstUpdate(true), - mRectangle(NULL), - mImpulse(NULL) +namespace MWRender { - Ogre::AxisAlignedBox aabInf; - aabInf.setInfinite(); - - mSceneMgr = Ogre::Root::getSingleton().createSceneManager(Ogre::ST_GENERIC); - - mCamera = mSceneMgr->createCamera("RippleCamera"); - - mRectangle = new Ogre::Rectangle2D(true); - mRectangle->setBoundingBox(aabInf); - mRectangle->setCorners(-1.0, 1.0, 1.0, -1.0, false); - Ogre::SceneNode* node = mSceneMgr->getRootSceneNode()->createChildSceneNode(); - node->attachObject(mRectangle); - mImpulse = new Ogre::Rectangle2D(true); - mImpulse->setCorners(-0.1, 0.1, 0.1, -0.1, false); - mImpulse->setBoundingBox(aabInf); - mImpulse->setMaterial("AddImpulse"); - Ogre::SceneNode* impulseNode = mSceneMgr->getRootSceneNode()->createChildSceneNode(); - impulseNode->attachObject(mImpulse); - - //float w=0.05; - for (int i=0; i<4; ++i) - { - Ogre::TexturePtr texture; - if (i != 3) - texture = Ogre::TextureManager::getSingleton().createManual("RippleHeight" + Ogre::StringConverter::toString(i), - Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, Ogre::TEX_TYPE_2D, mTextureSize, mTextureSize, 1, 0, Ogre::PF_R8G8B8, Ogre::TU_RENDERTARGET); - else - texture = Ogre::TextureManager::getSingleton().createManual("RippleNormal", - Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, Ogre::TEX_TYPE_2D, mTextureSize, mTextureSize, 1, 0, Ogre::PF_R8G8B8, Ogre::TU_RENDERTARGET); - - - Ogre::RenderTexture* rt = texture->getBuffer()->getRenderTarget(); - rt->removeAllViewports(); - rt->addViewport(mCamera); - rt->setAutoUpdated(false); - rt->getViewport(0)->setClearEveryFrame(false); +RippleSimulation::RippleSimulation(Ogre::SceneManager* mainSceneManager, const MWWorld::Fallback* fallback) + : mSceneMgr(mainSceneManager) + , mParticleSystem(NULL) + , mSceneNode(NULL) +{ + mRippleLifeTime = fallback->getFallbackFloat("Water_RippleLifetime"); + mRippleRotSpeed = fallback->getFallbackFloat("Water_RippleRotSpeed"); - // debug overlay - /* - Ogre::Rectangle2D* debugOverlay = new Ogre::Rectangle2D(true); - debugOverlay->setCorners(w*2-1, 0.9, (w+0.18)*2-1, 0.4, false); - w += 0.2; - debugOverlay->setBoundingBox(aabInf); - Ogre::SceneNode* debugNode = mMainSceneMgr->getRootSceneNode()->createChildSceneNode(); - debugNode->attachObject(debugOverlay); + // Unknown: + // fallback=Water_RippleScale,0.15, 6.5 + // fallback=Water_RippleAlphas,0.7, 0.1, 0.01 - Ogre::MaterialPtr debugMaterial = Ogre::MaterialManager::getSingleton().create("RippleDebug" + Ogre::StringConverter::toString(i), - Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME); + // Instantiate from ripples.particle file + mParticleSystem = mSceneMgr->createParticleSystem("openmw/Ripples", "openmw/Ripples"); - if (i != 3) - debugMaterial->getTechnique(0)->getPass(0)->createTextureUnitState("RippleHeight" + Ogre::StringConverter::toString(i)); - else - debugMaterial->getTechnique(0)->getPass(0)->createTextureUnitState("RippleNormal"); - debugMaterial->getTechnique(0)->getPass(0)->setLightingEnabled(false); + mParticleSystem->setRenderQueueGroup(RQG_Ripples); + mParticleSystem->setVisibilityFlags(RV_Effects); - debugOverlay->setMaterial("RippleDebug" + Ogre::StringConverter::toString(i)); - */ + int rippleFrameCount = fallback->getFallbackInt("Water_RippleFrameCount"); + std::string tex = fallback->getFallbackString("Water_RippleTexture"); - mRenderTargets[i] = rt; - mTextures[i] = texture; - } + sh::MaterialInstance* mat = sh::Factory::getInstance().getMaterialInstance("openmw/Ripple"); + mat->setProperty("anim_texture2", sh::makeProperty(new sh::StringValue(std::string("textures\\water\\") + tex + ".dds " + + Ogre::StringConverter::toString(rippleFrameCount) + + " " + + Ogre::StringConverter::toString(0.3)))); - sh::Factory::getInstance().setSharedParameter("rippleTextureSize", sh::makeProperty( - new sh::Vector4(1.0/512, 1.0/512, 512, 512))); - sh::Factory::getInstance().setSharedParameter("rippleCenter", sh::makeProperty( - new sh::Vector3(0, 0, 0))); - sh::Factory::getInstance().setSharedParameter("rippleAreaLength", sh::makeProperty( - new sh::FloatValue(mRippleAreaLength))); + // seems to be required to allocate mFreeParticles. TODO: patch Ogre to handle this better + mParticleSystem->_update(0.f); + mSceneNode = mSceneMgr->getRootSceneNode()->createChildSceneNode(); + mSceneNode->attachObject(mParticleSystem); } RippleSimulation::~RippleSimulation() { - delete mRectangle; - delete mImpulse; + if (mParticleSystem) + mSceneMgr->destroyParticleSystem(mParticleSystem); + mParticleSystem = NULL; - Ogre::Root::getSingleton().destroySceneManager(mSceneMgr); + if (mSceneNode) + mSceneMgr->destroySceneNode(mSceneNode); + mSceneNode = NULL; } void RippleSimulation::update(float dt, Ogre::Vector2 position) { - // try to keep 20 fps - mTime += dt; - - while (mTime >= 1/20.0 || mFirstUpdate) - { - mPreviousFrameOffset = mCurrentFrameOffset; - - mCurrentFrameOffset = position - mRippleCenter; - // add texel offsets from previous frame. - mCurrentFrameOffset += mTexelOffset; - - mTexelOffset = Ogre::Vector2(std::fmod(mCurrentFrameOffset.x, 1.0f/mTextureSize), - std::fmod(mCurrentFrameOffset.y, 1.0f/mTextureSize)); - - // now subtract new offset in order to snap to texels - mCurrentFrameOffset -= mTexelOffset; - - // texture coordinate space - mCurrentFrameOffset /= mRippleAreaLength; - - mRippleCenter = position; - - addImpulses(); - waterSimulation(); - heightMapToNormalMap(); - - swapHeightMaps(); - if (!mFirstUpdate) - mTime -= 1/20.0; - else - mFirstUpdate = false; - } - - sh::Factory::getInstance().setSharedParameter("rippleCenter", sh::makeProperty( - new sh::Vector3(mRippleCenter.x + mTexelOffset.x, mRippleCenter.y + mTexelOffset.y, 0))); -} - -void RippleSimulation::addImpulses() -{ - mRectangle->setVisible(false); - mImpulse->setVisible(true); - - /// \todo it should be more efficient to render all emitters at once + bool newParticle = false; for (std::vector::iterator it=mEmitters.begin(); it !=mEmitters.end(); ++it) { if (it->mPtr == MWBase::Environment::get().getWorld ()->getPlayerPtr()) @@ -165,69 +75,50 @@ void RippleSimulation::addImpulses() // for non-player actors this is done in updateObjectCell it->mPtr = MWBase::Environment::get().getWorld ()->getPlayerPtr(); } - const float* _currentPos = it->mPtr.getRefData().getPosition().pos; - Ogre::Vector3 currentPos (_currentPos[0], _currentPos[1], _currentPos[2]); - - if ( (currentPos - it->mLastEmitPosition).length() > 2 - && MWBase::Environment::get().getWorld ()->isUnderwater (it->mPtr.getCell(), currentPos)) + Ogre::Vector3 currentPos (it->mPtr.getRefData().getPosition().pos); + currentPos.z = 0; + if ( (currentPos - it->mLastEmitPosition).length() > 10 + // Only emit when close to the water surface, not above it and not too deep in the water + && MWBase::Environment::get().getWorld ()->isUnderwater (it->mPtr.getCell(), + Ogre::Vector3(it->mPtr.getRefData().getPosition().pos)) + && !MWBase::Environment::get().getWorld()->isSubmerged(it->mPtr)) { it->mLastEmitPosition = currentPos; - Ogre::Vector2 pos (currentPos.x, currentPos.y); - pos -= mRippleCenter; - pos /= mRippleAreaLength; - float size = mImpulseSize / mRippleAreaLength; - mImpulse->setCorners(pos.x-size, pos.y+size, pos.x+size, pos.y-size, false); - - // don't render if we are offscreen - if (pos.x - size >= 1.0 || pos.y+size <= -1.0 || pos.x+size <= -1.0 || pos.y-size >= 1.0) - continue; - mRenderTargets[1]->update(); + newParticle = true; + Ogre::Particle* created = mParticleSystem->createParticle(); + if (!created) + break; // TODO: cleanup the oldest particle to make room +#if OGRE_VERSION >= (1 << 16 | 10 << 8 | 0) + Ogre::Vector3& position = created->mPosition; + Ogre::Vector3& direction = created->mDirection; + Ogre::ColourValue& colour = created->mColour; + float& totalTimeToLive = created->mTotalTimeToLive; + float& timeToLive = created->mTimeToLive; + Ogre::Radian& rotSpeed = created->mRotationSpeed; + Ogre::Radian& rotation = created->mRotation; +#else + Ogre::Vector3& position = created->position; + Ogre::Vector3& direction = created->direction; + Ogre::ColourValue& colour = created->colour; + float& totalTimeToLive = created->totalTimeToLive; + float& timeToLive = created->timeToLive; + Ogre::Radian& rotSpeed = created->rotationSpeed; + Ogre::Radian& rotation = created->rotation; +#endif + timeToLive = totalTimeToLive = mRippleLifeTime; + colour = Ogre::ColourValue(0.f, 0.f, 0.f, 0.7); // Water_RippleAlphas.x? + direction = Ogre::Vector3(0,0,0); + position = currentPos; + position.z = 0; // Z is set by the Scene Node + rotSpeed = mRippleRotSpeed; + rotation = Ogre::Radian(Ogre::Math::RangeRandom(-Ogre::Math::PI, Ogre::Math::PI)); + created->setDimensions(mParticleSystem->getDefaultWidth(), mParticleSystem->getDefaultHeight()); } } - mImpulse->setVisible(false); - mRectangle->setVisible(true); -} - -void RippleSimulation::waterSimulation() -{ - mRectangle->setMaterial("HeightmapSimulation"); - - sh::Factory::getInstance().setTextureAlias("Heightmap0", mTextures[0]->getName()); - sh::Factory::getInstance().setTextureAlias("Heightmap1", mTextures[1]->getName()); - - sh::Factory::getInstance().setSharedParameter("currentFrameOffset", sh::makeProperty( - new sh::Vector3(mCurrentFrameOffset.x, mCurrentFrameOffset.y, 0))); - sh::Factory::getInstance().setSharedParameter("previousFrameOffset", sh::makeProperty( - new sh::Vector3(mPreviousFrameOffset.x, mPreviousFrameOffset.y, 0))); - - mRenderTargets[2]->update(); -} - -void RippleSimulation::heightMapToNormalMap() -{ - mRectangle->setMaterial("HeightToNormalMap"); - - sh::Factory::getInstance().setTextureAlias("Heightmap2", mTextures[2]->getName()); - - mRenderTargets[TEX_NORMAL]->update(); -} - -void RippleSimulation::swapHeightMaps() -{ - // 0 -> 1 -> 2 to 2 -> 0 ->1 - Ogre::RenderTexture* tmp = mRenderTargets[0]; - Ogre::TexturePtr tmp2 = mTextures[0]; - - mRenderTargets[0] = mRenderTargets[1]; - mTextures[0] = mTextures[1]; - - mRenderTargets[1] = mRenderTargets[2]; - mTextures[1] = mTextures[2]; - - mRenderTargets[2] = tmp; - mTextures[2] = tmp2; + if (newParticle) // now apparently needs another update, otherwise it won't render in the first frame after a particle is created. TODO: patch Ogre to handle this better + mParticleSystem->_update(0.f); } void RippleSimulation::addEmitter(const MWWorld::Ptr& ptr, float scale, float force) @@ -264,5 +155,15 @@ void RippleSimulation::updateEmitterPtr (const MWWorld::Ptr& old, const MWWorld: } } +void RippleSimulation::setWaterHeight(float height) +{ + mSceneNode->setPosition(0,0,height); +} + +void RippleSimulation::clear() +{ + mParticleSystem->clear(); +} + } diff --git a/apps/openmw/mwrender/ripplesimulation.hpp b/apps/openmw/mwrender/ripplesimulation.hpp index 7e7eebc1c..4551476a4 100644 --- a/apps/openmw/mwrender/ripplesimulation.hpp +++ b/apps/openmw/mwrender/ripplesimulation.hpp @@ -1,19 +1,19 @@ #ifndef RIPPLE_SIMULATION_H #define RIPPLE_SIMULATION_H -#include -#include -#include #include #include "../mwworld/ptr.hpp" namespace Ogre { - class RenderTexture; - class Camera; class SceneManager; - class Rectangle2D; + class ParticleSystem; +} + +namespace MWWorld +{ + class Fallback; } namespace MWRender @@ -30,9 +30,11 @@ struct Emitter class RippleSimulation { public: - RippleSimulation(Ogre::SceneManager* mainSceneManager); + RippleSimulation(Ogre::SceneManager* mainSceneManager, const MWWorld::Fallback* fallback); ~RippleSimulation(); + /// @param dt Time since the last frame + /// @param position Position of the player void update(float dt, Ogre::Vector2 position); /// adds an emitter, position will be tracked automatically @@ -40,44 +42,21 @@ public: void removeEmitter (const MWWorld::Ptr& ptr); void updateEmitterPtr (const MWWorld::Ptr& old, const MWWorld::Ptr& ptr); -private: - std::vector mEmitters; - - Ogre::RenderTexture* mRenderTargets[4]; - Ogre::TexturePtr mTextures[4]; - - int mTextureSize; - float mRippleAreaLength; - float mImpulseSize; - - bool mFirstUpdate; + /// Change the height of the water surface, thus moving all ripples with it + void setWaterHeight(float height); - Ogre::Camera* mCamera; + /// Remove all active ripples + void clear(); - // own scenemanager to render our simulation +private: Ogre::SceneManager* mSceneMgr; - Ogre::Rectangle2D* mRectangle; - - // scenemanager to create the debug overlays on - Ogre::SceneManager* mMainSceneMgr; - - static const int TEX_NORMAL = 3; - - Ogre::Rectangle2D* mImpulse; + Ogre::ParticleSystem* mParticleSystem; + Ogre::SceneNode* mSceneNode; - void addImpulses(); - void heightMapToNormalMap(); - void waterSimulation(); - void swapHeightMaps(); - - float mTime; - - Ogre::Vector2 mRippleCenter; - - Ogre::Vector2 mTexelOffset; + std::vector mEmitters; - Ogre::Vector2 mCurrentFrameOffset; - Ogre::Vector2 mPreviousFrameOffset; + float mRippleLifeTime; + float mRippleRotSpeed; }; } diff --git a/apps/openmw/mwrender/shadows.cpp b/apps/openmw/mwrender/shadows.cpp index 33e337649..5a6ccaca6 100644 --- a/apps/openmw/mwrender/shadows.cpp +++ b/apps/openmw/mwrender/shadows.cpp @@ -20,10 +20,9 @@ using namespace Ogre; using namespace MWRender; Shadows::Shadows(OEngine::Render::OgreRenderer* rend) : + mRendering(rend), mSceneMgr(rend->getScene()), mPSSMSetup(NULL), mShadowFar(1000), mFadeStart(0.9) { - mRendering = rend; - mSceneMgr = mRendering->getScene(); recreate(); } @@ -186,13 +185,3 @@ PSSMShadowCameraSetup* Shadows::getPSSMSetup() { return mPSSMSetup; } - -float Shadows::getShadowFar() const -{ - return mShadowFar; -} - -float Shadows::getFadeStart() const -{ - return mFadeStart; -} diff --git a/apps/openmw/mwrender/shadows.hpp b/apps/openmw/mwrender/shadows.hpp index bc2b141f7..fe125f54c 100644 --- a/apps/openmw/mwrender/shadows.hpp +++ b/apps/openmw/mwrender/shadows.hpp @@ -23,8 +23,6 @@ namespace MWRender void recreate(); Ogre::PSSMShadowCameraSetup* getPSSMSetup(); - float getShadowFar() const; - float getFadeStart() const; protected: OEngine::Render::OgreRenderer* mRendering; diff --git a/apps/openmw/mwrender/sky.cpp b/apps/openmw/mwrender/sky.cpp index 8354cca5d..f6287de5e 100644 --- a/apps/openmw/mwrender/sky.cpp +++ b/apps/openmw/mwrender/sky.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -19,6 +20,7 @@ #include #include +#include #include @@ -33,11 +35,47 @@ using namespace MWRender; using namespace Ogre; +namespace +{ + +void setAlpha (NifOgre::ObjectScenePtr scene, Ogre::MovableObject* movable, float alpha) +{ + Ogre::MaterialPtr mat = scene->mMaterialControllerMgr.getWritableMaterial(movable); + Ogre::Material::TechniqueIterator techs = mat->getTechniqueIterator(); + while(techs.hasMoreElements()) + { + Ogre::Technique *tech = techs.getNext(); + Ogre::Technique::PassIterator passes = tech->getPassIterator(); + while(passes.hasMoreElements()) + { + Ogre::Pass *pass = passes.getNext(); + Ogre::ColourValue diffuse = pass->getDiffuse(); + diffuse.a = alpha; + pass->setDiffuse(diffuse); + } + } + +} + +void setAlpha (NifOgre::ObjectScenePtr scene, float alpha) +{ + for(size_t i = 0; i < scene->mParticles.size(); ++i) + setAlpha(scene, scene->mParticles[i], alpha); + for(size_t i = 0; i < scene->mEntities.size(); ++i) + { + if (scene->mEntities[i] != scene->mSkelBase) + setAlpha(scene, scene->mEntities[i], alpha); + } +} + +} + BillboardObject::BillboardObject( const String& textureName, const float initialSize, const Vector3& position, SceneNode* rootNode, const std::string& material) +: mVisibility(1.0f) { SceneManager* sceneMgr = rootNode->getCreator(); @@ -67,10 +105,6 @@ BillboardObject::BillboardObject( const String& textureName, bodyCount++; } -BillboardObject::BillboardObject() -{ -} - void BillboardObject::requestedConfiguration (sh::MaterialInstance* m, const std::string& configuration) { } @@ -149,9 +183,12 @@ Moon::Moon( const String& textureName, SceneNode* rootNode, const std::string& material) : BillboardObject(textureName, initialSize, position, rootNode, material) + , mType(Type_Masser) { setVisibility(1.0); + mMaterial->setProperty("alphatexture", sh::makeProperty(new sh::StringValue(textureName + "_alpha"))); + mPhase = Moon::Phase_Full; } @@ -180,18 +217,19 @@ void Moon::setPhase(const Moon::Phase& phase) textureName += ".dds"; if (mType == Moon::Type_Secunda) + { sh::Factory::getInstance ().setTextureAlias ("secunda_texture", textureName); + sh::Factory::getInstance ().setTextureAlias ("secunda_texture_alpha", "textures\\tx_mooncircle_full_s.dds"); + } else + { sh::Factory::getInstance ().setTextureAlias ("masser_texture", textureName); + sh::Factory::getInstance ().setTextureAlias ("masser_texture_alpha", "textures\\tx_mooncircle_full_m.dds"); + } mPhase = phase; } -Moon::Phase Moon::getPhase() const -{ - return mPhase; -} - unsigned int Moon::getPhaseInt() const { if (mPhase == Moon::Phase_New) return 0; @@ -226,6 +264,7 @@ SkyManager::SkyManager(Ogre::SceneNode *root, Ogre::Camera *pCamera) , mCloudOpacity(0.0f) , mCloudSpeed(0.0f) , mStarsOpacity(0.0f) + , mLightning(NULL) , mRemainingTransitionTime(0.0f) , mGlareFade(0.0f) , mGlare(0.0f) @@ -294,7 +333,12 @@ void SkyManager::create() // Stars mAtmosphereNight = mRootNode->createChildSceneNode(); - NifOgre::ObjectScenePtr objects = NifOgre::Loader::createObjects(mAtmosphereNight, "meshes\\sky_night_01.nif"); + NifOgre::ObjectScenePtr objects; + if (Ogre::ResourceGroupManager::getSingleton().resourceExistsInAnyGroup("meshes\\sky_night_02.nif")) + objects = NifOgre::Loader::createObjects(mAtmosphereNight, "meshes\\sky_night_02.nif"); + else + objects = NifOgre::Loader::createObjects(mAtmosphereNight, "meshes\\sky_night_01.nif"); + for(size_t i = 0, matidx = 0;i < objects->mEntities.size();i++) { Entity* night1_ent = objects->mEntities[i]; @@ -381,9 +425,7 @@ void SkyManager::clearRain() for (std::map::iterator it = mRainModels.begin(); it != mRainModels.end();) { it->second.setNull(); - Ogre::SceneNode* parent = it->first->getParentSceneNode(); mSceneMgr->destroySceneNode(it->first); - mSceneMgr->destroySceneNode(parent); mRainModels.erase(it++); } } @@ -398,12 +440,13 @@ void SkyManager::updateRain(float dt) Ogre::Vector3 pos = it->first->getPosition(); pos.z -= mRainSpeed * dt; it->first->setPosition(pos); - if (pos.z < -minHeight) + if (pos.z < -minHeight + // Here we might want to add a "splash" effect later + || MWBase::Environment::get().getWorld()->isUnderwater( + MWBase::Environment::get().getWorld()->getPlayerPtr().getCell(), it->first->_getDerivedPosition())) { it->second.setNull(); - Ogre::SceneNode* parent = it->first->getParentSceneNode(); mSceneMgr->destroySceneNode(it->first); - mSceneMgr->destroySceneNode(parent); mRainModels.erase(it++); } else @@ -412,7 +455,6 @@ void SkyManager::updateRain(float dt) // Spawn new rain float rainFrequency = mRainFrequency; - float startHeight = 700; if (mRainEnabled) { mRainTimer += dt; @@ -420,16 +462,26 @@ void SkyManager::updateRain(float dt) { mRainTimer = 0; + // TODO: handle rain settings from Morrowind.ini const float rangeRandom = 100; float xOffs = (std::rand()/(RAND_MAX+1.0)) * rangeRandom - (rangeRandom/2); float yOffs = (std::rand()/(RAND_MAX+1.0)) * rangeRandom - (rangeRandom/2); - Ogre::SceneNode* sceneNode = mCamera->getParentSceneNode()->createChildSceneNode(); - sceneNode->setInheritOrientation(false); // Create a separate node to control the offset, since a node with setInheritOrientation(false) will still // consider the orientation of the parent node for its position, just not for its orientation - Ogre::SceneNode* offsetNode = sceneNode->createChildSceneNode(Ogre::Vector3(xOffs,yOffs,startHeight)); - + float startHeight = 700; + Ogre::Vector3 worldPos = mParticleNode->_getDerivedPosition(); + worldPos += Ogre::Vector3(xOffs, yOffs, startHeight); + if (MWBase::Environment::get().getWorld()->isUnderwater( + MWBase::Environment::get().getWorld()->getPlayerPtr().getCell(), worldPos)) + return; + + Ogre::SceneNode* offsetNode = mParticleNode->createChildSceneNode(Ogre::Vector3(xOffs,yOffs,startHeight)); + + // Spawn a new rain object for each instance. + // TODO: this is inefficient. We could try to use an Ogre::ParticleSystem instead, but then we would need to make assumptions + // about the rain meshes being Quads and their dimensions. + // Or we could clone meshes into one vertex buffer manually. NifOgre::ObjectScenePtr objects = NifOgre::Loader::createObjects(offsetNode, mRainEffect); for (unsigned int i=0; imEntities.size(); ++i) { @@ -456,6 +508,30 @@ void SkyManager::update(float duration) for (unsigned int i=0; imControllers.size(); ++i) mParticle->mControllers[i].update(); + for (unsigned int i=0; imParticles.size(); ++i) + { + Ogre::ParticleSystem* psys = mParticle->mParticles[i]; + Ogre::ParticleIterator pi = psys->_getIterator(); + while (!pi.end()) + { + Ogre::Particle *p = pi.getNext(); + #if OGRE_VERSION >= (1 << 16 | 10 << 8 | 0) + Ogre::Vector3 pos = p->mPosition; + Ogre::Real& timeToLive = p->mTimeToLive; + #else + Ogre::Vector3 pos = p->position; + Ogre::Real& timeToLive = p->timeToLive; + #endif + + if (psys->getKeepParticlesInLocalSpace() && psys->getParentNode()) + pos = psys->getParentNode()->convertLocalToWorldPosition(pos); + + if (MWBase::Environment::get().getWorld()->isUnderwater( + MWBase::Environment::get().getWorld()->getPlayerPtr().getCell(), pos)) + timeToLive = 0; + } + } + if (mIsStorm) mParticleNode->setOrientation(Ogre::Vector3::UNIT_Y.getRotationTo(mStormDirection)); } @@ -561,12 +637,6 @@ void SkyManager::setWeather(const MWWorld::WeatherResult& weather) } else { - if (!mParticleNode) - { - mParticleNode = mCamera->getParentSceneNode()->createChildSceneNode(); - mParticleNode->setInheritOrientation(false); - } - mParticle = NifOgre::Loader::createObjects(mParticleNode, mCurrentParticleEffect); for(size_t i = 0; i < mParticle->mParticles.size(); ++i) { @@ -584,13 +654,13 @@ void SkyManager::setWeather(const MWWorld::WeatherResult& weather) if (mClouds != weather.mCloudTexture) { - sh::Factory::getInstance().setTextureAlias ("cloud_texture_1", "textures\\"+weather.mCloudTexture); + sh::Factory::getInstance().setTextureAlias ("cloud_texture_1", Misc::ResourceHelpers::correctTexturePath(weather.mCloudTexture)); mClouds = weather.mCloudTexture; } if (mNextClouds != weather.mNextCloudTexture) { - sh::Factory::getInstance().setTextureAlias ("cloud_texture_2", "textures\\"+weather.mNextCloudTexture); + sh::Factory::getInstance().setTextureAlias ("cloud_texture_2", Misc::ResourceHelpers::correctTexturePath(weather.mNextCloudTexture)); mNextClouds = weather.mNextCloudTexture; } @@ -664,6 +734,11 @@ void SkyManager::setWeather(const MWWorld::WeatherResult& weather) mSun->setVisibility(weather.mGlareView * strength); mAtmosphereNight->setVisible(weather.mNight && mEnabled); + + if (mParticle.get()) + setAlpha(mParticle, weather.mEffectFade); + for (std::map::iterator it = mRainModels.begin(); it != mRainModels.end(); ++it) + setAlpha(it->second, weather.mEffectFade); } void SkyManager::setGlare(const float glare) @@ -692,14 +767,14 @@ void SkyManager::setStormDirection(const Vector3 &direction) mStormDirection = direction; } -void SkyManager::setSunDirection(const Vector3& direction, bool is_moon) +void SkyManager::setSunDirection(const Vector3& direction, bool is_night) { if (!mCreated) return; mSun->setPosition(direction); mSunGlare->setPosition(direction); float height = direction.z; - float fade = is_moon ? 0.0 : (( height > 0.5) ? 1.0 : height * 2); + float fade = is_night ? 0.0 : (( height > 0.5) ? 1.0 : height * 2); sh::Factory::getInstance ().setSharedParameter ("waterSunFade_sunHeight", sh::makeProperty(new sh::Vector2(fade, height))); } @@ -746,16 +821,6 @@ void SkyManager::setLightningStrength(const float factor) else mLightning->setVisible(false); } -void SkyManager::setLightningEnabled(bool enabled) -{ - /// \todo -} - -void SkyManager::setLightningDirection(const Ogre::Vector3& dir) -{ - if (!mCreated) return; - mLightning->setDirection (dir); -} void SkyManager::setMasserFade(const float fade) { @@ -792,3 +857,16 @@ void SkyManager::setGlareEnabled (bool enabled) return; mSunGlare->setVisible (mSunEnabled && enabled); } + +void SkyManager::attachToNode(SceneNode *sceneNode) +{ + if (!mParticleNode) + { + mParticleNode = sceneNode->createChildSceneNode(); + mParticleNode->setInheritOrientation(false); + } + else + { + sceneNode->addChild(mParticleNode); + } +} diff --git a/apps/openmw/mwrender/sky.hpp b/apps/openmw/mwrender/sky.hpp index 7c31150f3..6950dbab3 100644 --- a/apps/openmw/mwrender/sky.hpp +++ b/apps/openmw/mwrender/sky.hpp @@ -39,7 +39,6 @@ namespace MWRender Ogre::SceneNode* rootNode, const std::string& material ); - BillboardObject(); void requestedConfiguration (sh::MaterialInstance* m, const std::string& configuration); void createdConfiguration (sh::MaterialInstance* m, const std::string& configuration); @@ -103,7 +102,6 @@ namespace MWRender void setPhase(const Phase& phase); void setType(const Type& type); - Phase getPhase() const; unsigned int getPhaseInt() const; private: @@ -117,6 +115,9 @@ namespace MWRender SkyManager(Ogre::SceneNode* root, Ogre::Camera* pCamera); ~SkyManager(); + /// Attach weather particle effects to this scene node (should be the Camera's parent node) + void attachToNode(Ogre::SceneNode* sceneNode); + void update(float duration); void enable(); @@ -152,7 +153,7 @@ namespace MWRender void setStormDirection(const Ogre::Vector3& direction); - void setSunDirection(const Ogre::Vector3& direction, bool is_moon); + void setSunDirection(const Ogre::Vector3& direction, bool is_night); void setMasserDirection(const Ogre::Vector3& direction); @@ -169,8 +170,6 @@ namespace MWRender void secundaDisable(); void setLightningStrength(const float factor); - void setLightningDirection(const Ogre::Vector3& dir); - void setLightningEnabled(bool enabled); ///< disable prior to map render void setGlare(const float glare); void setGlareEnabled(bool enabled); diff --git a/apps/openmw/mwrender/terrainstorage.cpp b/apps/openmw/mwrender/terrainstorage.cpp index 2558c95c5..e74b6af12 100644 --- a/apps/openmw/mwrender/terrainstorage.cpp +++ b/apps/openmw/mwrender/terrainstorage.cpp @@ -1,24 +1,30 @@ #include "terrainstorage.hpp" -#include -#include -#include -#include -#include -#include -#include - #include #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwworld/esmstore.hpp" -#include - namespace MWRender { + TerrainStorage::TerrainStorage(bool preload) + { + if (preload) + { + const MWWorld::ESMStore &esmStore = + MWBase::Environment::get().getWorld()->getStore(); + + MWWorld::Store::iterator it = esmStore.get().begin(); + for (; it != esmStore.get().end(); ++it) + { + ESM::Land* land = const_cast(&*it); // TODO: fix store interface + land->loadData(ESM::Land::DATA_VCLR|ESM::Land::DATA_VHGT|ESM::Land::DATA_VNML|ESM::Land::DATA_VTEX); + } + } + } + void TerrainStorage::getBounds(float& minX, float& maxX, float& minY, float& maxY) { minX = 0, minY = 0, maxX = 0, maxY = 0; @@ -49,6 +55,12 @@ namespace MWRender const MWWorld::ESMStore &esmStore = MWBase::Environment::get().getWorld()->getStore(); ESM::Land* land = esmStore.get().search(cellX, cellY); + if (!land) + return NULL; + + const int flags = ESM::Land::DATA_VCLR|ESM::Land::DATA_VHGT|ESM::Land::DATA_VNML|ESM::Land::DATA_VTEX; + if (!land->isDataLoaded(flags)) + land->loadData(flags); return land; } @@ -59,515 +71,4 @@ namespace MWRender return esmStore.get().find(index, plugin); } - bool TerrainStorage::getMinMaxHeights(float size, const Ogre::Vector2 ¢er, float &min, float &max) - { - assert (size <= 1 && "TerrainStorage::getMinMaxHeights, chunk size should be <= 1 cell"); - - /// \todo investigate if min/max heights should be stored at load time in ESM::Land instead - - Ogre::Vector2 origin = center - Ogre::Vector2(size/2.f, size/2.f); - - assert(origin.x == (int) origin.x); - assert(origin.y == (int) origin.y); - - int cellX = origin.x; - int cellY = origin.y; - - const ESM::Land* land = getLand(cellX, cellY); - if (!land) - return false; - - min = std::numeric_limits().max(); - max = -std::numeric_limits().max(); - for (int row=0; rowmLandData->mHeights[col*ESM::Land::LAND_SIZE+row]; - if (h > max) - max = h; - if (h < min) - min = h; - } - } - return true; - } - - void TerrainStorage::fixNormal (Ogre::Vector3& normal, int cellX, int cellY, int col, int row) - { - while (col >= ESM::Land::LAND_SIZE-1) - { - ++cellY; - col -= ESM::Land::LAND_SIZE-1; - } - while (row >= ESM::Land::LAND_SIZE-1) - { - ++cellX; - row -= ESM::Land::LAND_SIZE-1; - } - while (col < 0) - { - --cellY; - col += ESM::Land::LAND_SIZE-1; - } - while (row < 0) - { - --cellX; - row += ESM::Land::LAND_SIZE-1; - } - ESM::Land* land = getLand(cellX, cellY); - if (land && land->mHasData) - { - normal.x = land->mLandData->mNormals[col*ESM::Land::LAND_SIZE*3+row*3]; - normal.y = land->mLandData->mNormals[col*ESM::Land::LAND_SIZE*3+row*3+1]; - normal.z = land->mLandData->mNormals[col*ESM::Land::LAND_SIZE*3+row*3+2]; - normal.normalise(); - } - else - normal = Ogre::Vector3(0,0,1); - } - - void TerrainStorage::averageNormal(Ogre::Vector3 &normal, int cellX, int cellY, int col, int row) - { - Ogre::Vector3 n1,n2,n3,n4; - fixNormal(n1, cellX, cellY, col+1, row); - fixNormal(n2, cellX, cellY, col-1, row); - fixNormal(n3, cellX, cellY, col, row+1); - fixNormal(n4, cellX, cellY, col, row-1); - normal = (n1+n2+n3+n4); - normal.normalise(); - } - - void TerrainStorage::fixColour (Ogre::ColourValue& color, int cellX, int cellY, int col, int row) - { - if (col == ESM::Land::LAND_SIZE-1) - { - ++cellY; - col = 0; - } - if (row == ESM::Land::LAND_SIZE-1) - { - ++cellX; - row = 0; - } - ESM::Land* land = getLand(cellX, cellY); - if (land && land->mLandData->mUsingColours) - { - color.r = land->mLandData->mColours[col*ESM::Land::LAND_SIZE*3+row*3] / 255.f; - color.g = land->mLandData->mColours[col*ESM::Land::LAND_SIZE*3+row*3+1] / 255.f; - color.b = land->mLandData->mColours[col*ESM::Land::LAND_SIZE*3+row*3+2] / 255.f; - } - else - { - color.r = 1; - color.g = 1; - color.b = 1; - } - - } - - void TerrainStorage::fillVertexBuffers (int lodLevel, float size, const Ogre::Vector2& center, Terrain::Alignment align, - std::vector& positions, - std::vector& normals, - std::vector& colours) - { - // LOD level n means every 2^n-th vertex is kept - size_t increment = 1 << lodLevel; - - Ogre::Vector2 origin = center - Ogre::Vector2(size/2.f, size/2.f); - assert(origin.x == (int) origin.x); - assert(origin.y == (int) origin.y); - - int startX = origin.x; - int startY = origin.y; - - size_t numVerts = size*(ESM::Land::LAND_SIZE-1)/increment + 1; - - colours.resize(numVerts*numVerts*4); - positions.resize(numVerts*numVerts*3); - normals.resize(numVerts*numVerts*3); - - Ogre::Vector3 normal; - Ogre::ColourValue color; - - float vertY; - float vertX; - - float vertY_ = 0; // of current cell corner - for (int cellY = startY; cellY < startY + std::ceil(size); ++cellY) - { - float vertX_ = 0; // of current cell corner - for (int cellX = startX; cellX < startX + std::ceil(size); ++cellX) - { - ESM::Land* land = getLand(cellX, cellY); - if (land && !land->mHasData) - land = NULL; - bool hasColors = land && land->mLandData->mUsingColours; - - int rowStart = 0; - int colStart = 0; - // Skip the first row / column unless we're at a chunk edge, - // since this row / column is already contained in a previous cell - if (colStart == 0 && vertY_ != 0) - colStart += increment; - if (rowStart == 0 && vertX_ != 0) - rowStart += increment; - - vertY = vertY_; - for (int col=colStart; colmLandData->mHeights[col*ESM::Land::LAND_SIZE+row]; - else - positions[vertX*numVerts*3 + vertY*3 + 2] = -2048; - - if (land) - { - normal.x = land->mLandData->mNormals[col*ESM::Land::LAND_SIZE*3+row*3]; - normal.y = land->mLandData->mNormals[col*ESM::Land::LAND_SIZE*3+row*3+1]; - normal.z = land->mLandData->mNormals[col*ESM::Land::LAND_SIZE*3+row*3+2]; - normal.normalise(); - } - else - normal = Ogre::Vector3(0,0,1); - - // Normals apparently don't connect seamlessly between cells - if (col == ESM::Land::LAND_SIZE-1 || row == ESM::Land::LAND_SIZE-1) - fixNormal(normal, cellX, cellY, col, row); - - // some corner normals appear to be complete garbage (z < 0) - if ((row == 0 || row == ESM::Land::LAND_SIZE-1) && (col == 0 || col == ESM::Land::LAND_SIZE-1)) - averageNormal(normal, cellX, cellY, col, row); - - assert(normal.z > 0); - - normals[vertX*numVerts*3 + vertY*3] = normal.x; - normals[vertX*numVerts*3 + vertY*3 + 1] = normal.y; - normals[vertX*numVerts*3 + vertY*3 + 2] = normal.z; - - if (hasColors) - { - color.r = land->mLandData->mColours[col*ESM::Land::LAND_SIZE*3+row*3] / 255.f; - color.g = land->mLandData->mColours[col*ESM::Land::LAND_SIZE*3+row*3+1] / 255.f; - color.b = land->mLandData->mColours[col*ESM::Land::LAND_SIZE*3+row*3+2] / 255.f; - } - else - { - color.r = 1; - color.g = 1; - color.b = 1; - } - - // Unlike normals, colors mostly connect seamlessly between cells, but not always... - if (col == ESM::Land::LAND_SIZE-1 || row == ESM::Land::LAND_SIZE-1) - fixColour(color, cellX, cellY, col, row); - - color.a = 1; - Ogre::uint32 rsColor; - Ogre::Root::getSingleton().getRenderSystem()->convertColourValue(color, &rsColor); - memcpy(&colours[vertX*numVerts*4 + vertY*4], &rsColor, sizeof(Ogre::uint32)); - - ++vertX; - } - ++vertY; - } - vertX_ = vertX; - } - vertY_ = vertY; - - assert(vertX_ == numVerts); // Ensure we covered whole area - } - assert(vertY_ == numVerts); // Ensure we covered whole area - } - - TerrainStorage::UniqueTextureId TerrainStorage::getVtexIndexAt(int cellX, int cellY, - int x, int y) - { - // For the first/last row/column, we need to get the texture from the neighbour cell - // to get consistent blending at the borders - --x; - if (x < 0) - { - --cellX; - x += ESM::Land::LAND_TEXTURE_SIZE; - } - if (y >= ESM::Land::LAND_TEXTURE_SIZE) // Y appears to be wrapped from the other side because why the hell not? - { - ++cellY; - y -= ESM::Land::LAND_TEXTURE_SIZE; - } - - assert(xmLandData->mTextures[y * ESM::Land::LAND_TEXTURE_SIZE + x]; - if (tex == 0) - return std::make_pair(0,0); // vtex 0 is always the base texture, regardless of plugin - return std::make_pair(tex, land->mPlugin); - } - else - return std::make_pair(0,0); - } - - std::string TerrainStorage::getTextureName(UniqueTextureId id) - { - if (id.first == 0) - return "_land_default.dds"; // Not sure if the default texture floatly is hardcoded? - - // NB: All vtex ids are +1 compared to the ltex ids - const ESM::LandTexture* ltex = getLandTexture(id.first-1, id.second); - - std::string texture = ltex->mTexture; - //TODO this is needed due to MWs messed up texture handling - texture = texture.substr(0, texture.rfind(".")) + ".dds"; - - return texture; - } - - void TerrainStorage::getBlendmaps (const std::vector& nodes, std::vector& out, bool pack) - { - for (std::vector::const_iterator it = nodes.begin(); it != nodes.end(); ++it) - { - out.push_back(Terrain::LayerCollection()); - out.back().mTarget = *it; - getBlendmapsImpl((*it)->getSize(), (*it)->getCenter(), pack, out.back().mBlendmaps, out.back().mLayers); - } - } - - void TerrainStorage::getBlendmaps(float chunkSize, const Ogre::Vector2 &chunkCenter, - bool pack, std::vector &blendmaps, std::vector &layerList) - { - getBlendmapsImpl(chunkSize, chunkCenter, pack, blendmaps, layerList); - } - - void TerrainStorage::getBlendmapsImpl(float chunkSize, const Ogre::Vector2 &chunkCenter, - bool pack, std::vector &blendmaps, std::vector &layerList) - { - // TODO - blending isn't completely right yet; the blending radius appears to be - // different at a cell transition (2 vertices, not 4), so we may need to create a larger blendmap - // and interpolate the rest of the cell by hand? :/ - - Ogre::Vector2 origin = chunkCenter - Ogre::Vector2(chunkSize/2.f, chunkSize/2.f); - int cellX = origin.x; - int cellY = origin.y; - - // Save the used texture indices so we know the total number of textures - // and number of required blend maps - std::set textureIndices; - // Due to the way the blending works, the base layer will always shine through in between - // blend transitions (eg halfway between two texels, both blend values will be 0.5, so 25% of base layer visible). - // To get a consistent look, we need to make sure to use the same base layer in all cells. - // So we're always adding _land_default.dds as the base layer here, even if it's not referenced in this cell. - textureIndices.insert(std::make_pair(0,0)); - - for (int y=0; y textureIndicesMap; - for (std::set::iterator it = textureIndices.begin(); it != textureIndices.end(); ++it) - { - int size = textureIndicesMap.size(); - textureIndicesMap[*it] = size; - layerList.push_back(getLayerInfo(getTextureName(*it))); - } - - int numTextures = textureIndices.size(); - // numTextures-1 since the base layer doesn't need blending - int numBlendmaps = pack ? std::ceil((numTextures-1) / 4.f) : (numTextures-1); - - int channels = pack ? 4 : 1; - - // Second iteration - create and fill in the blend maps - const int blendmapSize = ESM::Land::LAND_TEXTURE_SIZE+1; - - for (int i=0; isecond; - int blendIndex = (pack ? std::floor((layerIndex-1)/4.f) : layerIndex-1); - int channel = pack ? std::max(0, (layerIndex-1) % 4) : 0; - - if (blendIndex == i) - pData[y*blendmapSize*channels + x*channels + channel] = 255; - else - pData[y*blendmapSize*channels + x*channels + channel] = 0; - } - } - blendmaps.push_back(Ogre::PixelBox(blendmapSize, blendmapSize, 1, format, pData)); - } - } - - float TerrainStorage::getHeightAt(const Ogre::Vector3 &worldPos) - { - int cellX = std::floor(worldPos.x / 8192.f); - int cellY = std::floor(worldPos.y / 8192.f); - - ESM::Land* land = getLand(cellX, cellY); - if (!land) - return -2048; - - // Mostly lifted from Ogre::Terrain::getHeightAtTerrainPosition - - // Normalized position in the cell - float nX = (worldPos.x - (cellX * 8192))/8192.f; - float nY = (worldPos.y - (cellY * 8192))/8192.f; - - // get left / bottom points (rounded down) - float factor = ESM::Land::LAND_SIZE - 1.0f; - float invFactor = 1.0f / factor; - - int startX = static_cast(nX * factor); - int startY = static_cast(nY * factor); - int endX = startX + 1; - int endY = startY + 1; - - assert(endX < ESM::Land::LAND_SIZE); - assert(endY < ESM::Land::LAND_SIZE); - - // now get points in terrain space (effectively rounding them to boundaries) - float startXTS = startX * invFactor; - float startYTS = startY * invFactor; - float endXTS = endX * invFactor; - float endYTS = endY * invFactor; - - // get parametric from start coord to next point - float xParam = (nX - startXTS) * factor; - float yParam = (nY - startYTS) * factor; - - /* For even / odd tri strip rows, triangles are this shape: - even odd - 3---2 3---2 - | / | | \ | - 0---1 0---1 - */ - - // Build all 4 positions in normalized cell space, using point-sampled height - Ogre::Vector3 v0 (startXTS, startYTS, getVertexHeight(land, startX, startY) / 8192.f); - Ogre::Vector3 v1 (endXTS, startYTS, getVertexHeight(land, endX, startY) / 8192.f); - Ogre::Vector3 v2 (endXTS, endYTS, getVertexHeight(land, endX, endY) / 8192.f); - Ogre::Vector3 v3 (startXTS, endYTS, getVertexHeight(land, startX, endY) / 8192.f); - // define this plane in terrain space - Ogre::Plane plane; - // (At the moment, all rows have the same triangle alignment) - if (true) - { - // odd row - bool secondTri = ((1.0 - yParam) > xParam); - if (secondTri) - plane.redefine(v0, v1, v3); - else - plane.redefine(v1, v2, v3); - } - else - { - // even row - bool secondTri = (yParam > xParam); - if (secondTri) - plane.redefine(v0, v2, v3); - else - plane.redefine(v0, v1, v2); - } - - // Solve plane equation for z - return (-plane.normal.x * nX - -plane.normal.y * nY - - plane.d) / plane.normal.z * 8192; - - } - - float TerrainStorage::getVertexHeight(const ESM::Land *land, int x, int y) - { - assert(x < ESM::Land::LAND_SIZE); - assert(y < ESM::Land::LAND_SIZE); - return land->mLandData->mHeights[y * ESM::Land::LAND_SIZE + x]; - } - - Terrain::LayerInfo TerrainStorage::getLayerInfo(const std::string& texture) - { - // Already have this cached? - if (mLayerInfoMap.find(texture) != mLayerInfoMap.end()) - return mLayerInfoMap[texture]; - - Terrain::LayerInfo info; - info.mParallax = false; - info.mSpecular = false; - info.mDiffuseMap = "textures\\" + texture; - std::string texture_ = texture; - boost::replace_last(texture_, ".", "_nh."); - if (Ogre::ResourceGroupManager::getSingleton().resourceExistsInAnyGroup("textures\\" + texture_)) - { - info.mNormalMap = "textures\\" + texture_; - info.mParallax = true; - } - else - { - texture_ = texture; - boost::replace_last(texture_, ".", "_n."); - if (Ogre::ResourceGroupManager::getSingleton().resourceExistsInAnyGroup("textures\\" + texture_)) - info.mNormalMap = "textures\\" + texture_; - } - - texture_ = texture; - boost::replace_last(texture_, ".", "_diffusespec."); - if (Ogre::ResourceGroupManager::getSingleton().resourceExistsInAnyGroup("textures\\" + texture_)) - { - info.mDiffuseMap = "textures\\" + texture_; - info.mSpecular = true; - } - - // This wasn't cached, so the textures are probably not loaded either. - // Background load them so they are hopefully already loaded once we need them! - Ogre::ResourceBackgroundQueue::getSingleton().load("Texture", info.mDiffuseMap, "General"); - if (!info.mNormalMap.empty()) - Ogre::ResourceBackgroundQueue::getSingleton().load("Texture", info.mNormalMap, "General"); - - mLayerInfoMap[texture] = info; - - return info; - } - - Terrain::LayerInfo TerrainStorage::getDefaultLayer() - { - Terrain::LayerInfo info; - info.mDiffuseMap = "textures\\_land_default.dds"; - info.mParallax = false; - info.mSpecular = false; - return info; - } - - float TerrainStorage::getCellWorldSize() - { - return ESM::Land::REAL_SIZE; - } - - int TerrainStorage::getCellVertices() - { - return ESM::Land::LAND_SIZE; - } - } diff --git a/apps/openmw/mwrender/terrainstorage.hpp b/apps/openmw/mwrender/terrainstorage.hpp index c1fb74445..e6f4a04ad 100644 --- a/apps/openmw/mwrender/terrainstorage.hpp +++ b/apps/openmw/mwrender/terrainstorage.hpp @@ -1,109 +1,25 @@ #ifndef MWRENDER_TERRAINSTORAGE_H #define MWRENDER_TERRAINSTORAGE_H -#include -#include - -#include +#include namespace MWRender { - class TerrainStorage : public Terrain::Storage + /// @brief Connects the ESM Store used in OpenMW with the ESMTerrain storage. + class TerrainStorage : public ESMTerrain::Storage { private: virtual ESM::Land* getLand (int cellX, int cellY); virtual const ESM::LandTexture* getLandTexture(int index, short plugin); public: + ///@param preload Preload all Land records at startup? If using the multithreaded terrain component, this + /// should be set to "true" in order to avoid race conditions. + TerrainStorage(bool preload); + /// Get bounds of the whole terrain in cell units virtual void getBounds(float& minX, float& maxX, float& minY, float& maxY); - - /// Get the minimum and maximum heights of a terrain region. - /// @note Will only be called for chunks with size = minBatchSize, i.e. leafs of the quad tree. - /// Larger chunks can simply merge AABB of children. - /// @param size size of the chunk in cell units - /// @param center center of the chunk in cell units - /// @param min min height will be stored here - /// @param max max height will be stored here - /// @return true if there was data available for this terrain chunk - virtual bool getMinMaxHeights (float size, const Ogre::Vector2& center, float& min, float& max); - - /// Fill vertex buffers for a terrain chunk. - /// @note May be called from background threads. Make sure to only call thread-safe functions from here! - /// @note returned colors need to be in render-system specific format! Use RenderSystem::convertColourValue. - /// @param lodLevel LOD level, 0 = most detailed - /// @param size size of the terrain chunk in cell units - /// @param center center of the chunk in cell units - /// @param positions buffer to write vertices - /// @param normals buffer to write vertex normals - /// @param colours buffer to write vertex colours - virtual void fillVertexBuffers (int lodLevel, float size, const Ogre::Vector2& center, Terrain::Alignment align, - std::vector& positions, - std::vector& normals, - std::vector& colours); - - /// Create textures holding layer blend values for a terrain chunk. - /// @note The terrain chunk shouldn't be larger than one cell since otherwise we might - /// have to do a ridiculous amount of different layers. For larger chunks, composite maps should be used. - /// @note May be called from background threads. - /// @param chunkSize size of the terrain chunk in cell units - /// @param chunkCenter center of the chunk in cell units - /// @param pack Whether to pack blend values for up to 4 layers into one texture (one in each channel) - - /// otherwise, each texture contains blend values for one layer only. Shader-based rendering - /// can utilize packing, FFP can't. - /// @param blendmaps created blendmaps will be written here - /// @param layerList names of the layer textures used will be written here - virtual void getBlendmaps (float chunkSize, const Ogre::Vector2& chunkCenter, bool pack, - std::vector& blendmaps, - std::vector& layerList); - - /// Retrieve pixel data for textures holding layer blend values for terrain chunks and layer texture information. - /// This variant is provided to eliminate the overhead of virtual function calls when retrieving a large number of blendmaps at once. - /// @note The terrain chunks shouldn't be larger than one cell since otherwise we might - /// have to do a ridiculous amount of different layers. For larger chunks, composite maps should be used. - /// @note May be called from background threads. - /// @param nodes A collection of nodes for which to retrieve the aforementioned data - /// @param out Output vector - /// @param pack Whether to pack blend values for up to 4 layers into one texture (one in each channel) - - /// otherwise, each texture contains blend values for one layer only. Shader-based rendering - /// can utilize packing, FFP can't. - virtual void getBlendmaps (const std::vector& nodes, std::vector& out, bool pack); - - virtual float getHeightAt (const Ogre::Vector3& worldPos); - - virtual Terrain::LayerInfo getDefaultLayer(); - - /// Get the transformation factor for mapping cell units to world units. - virtual float getCellWorldSize(); - - /// Get the number of vertices on one side for each cell. Should be (power of two)+1 - virtual int getCellVertices(); - - private: - void fixNormal (Ogre::Vector3& normal, int cellX, int cellY, int col, int row); - void fixColour (Ogre::ColourValue& colour, int cellX, int cellY, int col, int row); - void averageNormal (Ogre::Vector3& normal, int cellX, int cellY, int col, int row); - - float getVertexHeight (const ESM::Land* land, int x, int y); - - // Since plugins can define new texture palettes, we need to know the plugin index too - // in order to retrieve the correct texture name. - // pair - typedef std::pair UniqueTextureId; - - UniqueTextureId getVtexIndexAt(int cellX, int cellY, - int x, int y); - std::string getTextureName (UniqueTextureId id); - - std::map mLayerInfoMap; - - Terrain::LayerInfo getLayerInfo(const std::string& texture); - - // Non-virtual - void getBlendmapsImpl (float chunkSize, const Ogre::Vector2& chunkCenter, bool pack, - std::vector& blendmaps, - std::vector& layerList); }; } diff --git a/apps/openmw/mwrender/videoplayer.cpp b/apps/openmw/mwrender/videoplayer.cpp deleted file mode 100644 index 409e27388..000000000 --- a/apps/openmw/mwrender/videoplayer.cpp +++ /dev/null @@ -1,1181 +0,0 @@ -#include "videoplayer.hpp" - -#define __STDC_CONSTANT_MACROS -#include - -#include -#include - -#include -#include -#include - -#include - -#include "../mwbase/environment.hpp" -#include "../mwbase/soundmanager.hpp" -#include "../mwsound/sound_decoder.hpp" -#include "../mwsound/sound.hpp" - -#ifdef _WIN32 -#include - -typedef SSIZE_T ssize_t; -#endif - -namespace MWRender -{ - -#ifdef OPENMW_USE_FFMPEG - -extern "C" -{ - #include - #include - #include - - // From libavformat version 55.0.100 and onward the declaration of av_gettime() is removed from libavformat/avformat.h and moved - // to libavutil/time.h - // https://github.com/FFmpeg/FFmpeg/commit/06a83505992d5f49846c18507a6c3eb8a47c650e - #if AV_VERSION_INT(55, 0, 100) <= AV_VERSION_INT(LIBAVFORMAT_VERSION_MAJOR, LIBAVFORMAT_VERSION_MINOR, LIBAVFORMAT_VERSION_MICRO) - #include - #endif - - // From libavutil version 52.2.0 and onward the declaration of - // AV_CH_LAYOUT_* is removed from libavcodec/avcodec.h and moved to - // libavutil/channel_layout.h - #if AV_VERSION_INT(52, 2, 0) <= AV_VERSION_INT(LIBAVUTIL_VERSION_MAJOR, \ - LIBAVUTIL_VERSION_MINOR, LIBAVUTIL_VERSION_MICRO) - #include - #endif -} - -#ifdef _WIN32 - // Decide whether to play binkaudio. - #include - // libavcodec versions 54.10.100 (or maybe earlier) to 54.54.100 potentially crashes Windows 64bit. - // From version 54.56 or higher, there's no sound due to the encoding format changing from S16 to FLTP - // (see https://gitorious.org/ffmpeg/ffmpeg/commit/7bfd1766d1c18f07b0a2dd042418a874d49ea60d and - // http://git.videolan.org/?p=ffmpeg.git;a=commitdiff;h=3049d5b9b32845c86aa5588bb3352bdeb2edfdb2;hp=43c6b45a53a186a187f7266e4d6bd3c2620519f1), - // but does not crash (or at least no known crash). - #if (LIBAVCODEC_VERSION_MAJOR > 54) - #define FFMPEG_PLAY_BINKAUDIO - #else - #ifdef _WIN64 - #if ((LIBAVCODEC_VERSION_MAJOR == 54) && (LIBAVCODEC_VERSION_MINOR >= 55)) - #define FFMPEG_PLAY_BINKAUDIO - #endif - #else - #if ((LIBAVCODEC_VERSION_MAJOR == 54) && (LIBAVCODEC_VERSION_MINOR >= 10)) - #define FFMPEG_PLAY_BINKAUDIO - #endif - #endif - #endif -#endif - -#define MAX_AUDIOQ_SIZE (5 * 16 * 1024) -#define MAX_VIDEOQ_SIZE (5 * 256 * 1024) -#define AV_SYNC_THRESHOLD 0.01 -#define AUDIO_DIFF_AVG_NB 20 -#define VIDEO_PICTURE_QUEUE_SIZE 1 - -enum { - AV_SYNC_AUDIO_MASTER, - AV_SYNC_VIDEO_MASTER, - AV_SYNC_EXTERNAL_MASTER, - - AV_SYNC_DEFAULT = AV_SYNC_EXTERNAL_MASTER -}; - - -struct PacketQueue { - PacketQueue() - : first_pkt(NULL), last_pkt(NULL), flushing(false), nb_packets(0), size(0) - { } - ~PacketQueue() - { clear(); } - - AVPacketList *first_pkt, *last_pkt; - volatile bool flushing; - int nb_packets; - int size; - - boost::mutex mutex; - boost::condition_variable cond; - - void put(AVPacket *pkt); - int get(AVPacket *pkt, VideoState *is); - - void flush(); - void clear(); -}; - -struct VideoPicture { - VideoPicture() : pts(0.0) - { } - - std::vector data; - double pts; -}; - -struct VideoState { - VideoState() - : format_ctx(NULL), av_sync_type(AV_SYNC_DEFAULT) - , external_clock_base(0.0) - , audio_st(NULL) - , video_st(NULL), frame_last_pts(0.0), frame_last_delay(0.0), - video_clock(0.0), sws_context(NULL), rgbaFrame(NULL), pictq_size(0), - pictq_rindex(0), pictq_windex(0) - , refresh_rate_ms(10), refresh(false), quit(false), display_ready(false) - { - // Register all formats and codecs - av_register_all(); - } - - ~VideoState() - { deinit(); } - - void init(const std::string& resourceName); - void deinit(); - - int stream_open(int stream_index, AVFormatContext *pFormatCtx); - - bool update(); - - static void video_thread_loop(VideoState *is); - static void decode_thread_loop(VideoState *is); - - void video_display(); - void video_refresh_timer(); - - int queue_picture(AVFrame *pFrame, double pts); - double synchronize_video(AVFrame *src_frame, double pts); - - static void video_refresh(VideoState *is); - - - double get_audio_clock() - { return this->AudioTrack->getTimeOffset(); } - - double get_video_clock() - { return this->frame_last_pts; } - - double get_external_clock() - { return ((uint64_t)av_gettime()-this->external_clock_base) / 1000000.0; } - - double get_master_clock() - { - if(this->av_sync_type == AV_SYNC_VIDEO_MASTER) - return this->get_video_clock(); - if(this->av_sync_type == AV_SYNC_AUDIO_MASTER) - return this->get_audio_clock(); - return this->get_external_clock(); - } - - - static int OgreResource_Read(void *user_data, uint8_t *buf, int buf_size); - static int OgreResource_Write(void *user_data, uint8_t *buf, int buf_size); - static int64_t OgreResource_Seek(void *user_data, int64_t offset, int whence); - - Ogre::TexturePtr mTexture; - - Ogre::DataStreamPtr stream; - AVFormatContext* format_ctx; - - int av_sync_type; - uint64_t external_clock_base; - - AVStream** audio_st; - PacketQueue audioq; - MWBase::SoundPtr AudioTrack; - - AVStream** video_st; - double frame_last_pts; - double frame_last_delay; - double video_clock; ///pkt = *pkt; - pkt1->next = NULL; - - if(pkt1->pkt.destruct == NULL) - { - if(av_dup_packet(&pkt1->pkt) < 0) - { - av_free(pkt1); - throw std::runtime_error("Failed to duplicate packet"); - } - av_free_packet(pkt); - } - - this->mutex.lock (); - - if(!last_pkt) - this->first_pkt = pkt1; - else - this->last_pkt->next = pkt1; - this->last_pkt = pkt1; - this->nb_packets++; - this->size += pkt1->pkt.size; - this->cond.notify_one(); - - this->mutex.unlock(); -} - -int PacketQueue::get(AVPacket *pkt, VideoState *is) -{ - boost::unique_lock lock(this->mutex); - while(!is->quit) - { - AVPacketList *pkt1 = this->first_pkt; - if(pkt1) - { - this->first_pkt = pkt1->next; - if(!this->first_pkt) - this->last_pkt = NULL; - this->nb_packets--; - this->size -= pkt1->pkt.size; - - *pkt = pkt1->pkt; - av_free(pkt1); - - return 1; - } - - if(this->flushing) - break; - this->cond.wait(lock); - } - - return -1; -} - -void PacketQueue::flush() -{ - this->flushing = true; - this->cond.notify_one(); -} - -void PacketQueue::clear() -{ - AVPacketList *pkt, *pkt1; - - this->mutex.lock(); - for(pkt = this->first_pkt; pkt != NULL; pkt = pkt1) - { - pkt1 = pkt->next; - av_free_packet(&pkt->pkt); - av_freep(&pkt); - } - this->last_pkt = NULL; - this->first_pkt = NULL; - this->nb_packets = 0; - this->size = 0; - this->mutex.unlock (); -} - - -class MovieAudioDecoder : public MWSound::Sound_Decoder -{ - static void fail(const std::string &str) - { - throw std::runtime_error(str); - } - - struct AutoAVPacket : public AVPacket { - AutoAVPacket(int size=0) - { - if(av_new_packet(this, size) < 0) - throw std::bad_alloc(); - } - ~AutoAVPacket() - { av_free_packet(this); } - }; - - VideoState *mVideoState; - AVStream *mAVStream; - - AutoAVPacket mPacket; - AVFrame *mFrame; - ssize_t mFramePos; - ssize_t mFrameSize; - - double mAudioClock; - - /* averaging filter for audio sync */ - double mAudioDiffAccum; - const double mAudioDiffAvgCoef; - const double mAudioDiffThreshold; - int mAudioDiffAvgCount; - - /* Add or subtract samples to get a better sync, return number of bytes to - * skip (negative means to duplicate). */ - int synchronize_audio() - { - if(mVideoState->av_sync_type == AV_SYNC_AUDIO_MASTER) - return 0; - - int sample_skip = 0; - - // accumulate the clock difference - double diff = mVideoState->get_master_clock() - mVideoState->get_audio_clock(); - mAudioDiffAccum = diff + mAudioDiffAvgCoef * mAudioDiffAccum; - if(mAudioDiffAvgCount < AUDIO_DIFF_AVG_NB) - mAudioDiffAvgCount++; - else - { - double avg_diff = mAudioDiffAccum * (1.0 - mAudioDiffAvgCoef); - if(fabs(avg_diff) >= mAudioDiffThreshold) - { - int n = av_get_bytes_per_sample(mAVStream->codec->sample_fmt) * - mAVStream->codec->channels; - sample_skip = ((int)(diff * mAVStream->codec->sample_rate) * n); - } - } - - return sample_skip; - } - - int audio_decode_frame(AVFrame *frame) - { - AVPacket *pkt = &mPacket; - - for(;;) - { - while(pkt->size > 0) - { - int len1, got_frame; - - len1 = avcodec_decode_audio4(mAVStream->codec, frame, &got_frame, pkt); - if(len1 < 0) break; - - if(len1 <= pkt->size) - { - /* Move the unread data to the front and clear the end bits */ - int remaining = pkt->size - len1; - memmove(pkt->data, &pkt->data[len1], remaining); - av_shrink_packet(pkt, remaining); - } - - /* No data yet? Look for more frames */ - if(!got_frame || frame->nb_samples <= 0) - continue; - - mAudioClock += (double)frame->nb_samples / - (double)mAVStream->codec->sample_rate; - - /* We have data, return it and come back for more later */ - return frame->nb_samples * mAVStream->codec->channels * - av_get_bytes_per_sample(mAVStream->codec->sample_fmt); - } - av_free_packet(pkt); - - /* next packet */ - if(mVideoState->audioq.get(pkt, mVideoState) < 0) - return -1; - - /* if update, update the audio clock w/pts */ - if((uint64_t)pkt->pts != AV_NOPTS_VALUE) - mAudioClock = av_q2d(mAVStream->time_base)*pkt->pts; - } - } - - void open(const std::string&) -#ifdef _WIN32 - { fail(std::string("Invalid call to ")+__FUNCSIG__); } -#else - { fail(std::string("Invalid call to ")+__PRETTY_FUNCTION__); } -#endif - - void close() { } - - std::string getName() - { return mVideoState->stream->getName(); } - - void rewind() { } - -public: - MovieAudioDecoder(VideoState *is) - : mVideoState(is) - , mAVStream(*is->audio_st) - , mFrame(avcodec_alloc_frame()) - , mFramePos(0) - , mFrameSize(0) - , mAudioClock(0.0) - , mAudioDiffAccum(0.0) - , mAudioDiffAvgCoef(exp(log(0.01 / AUDIO_DIFF_AVG_NB))) - /* Correct audio only if larger error than this */ - , mAudioDiffThreshold(2.0 * 0.050/* 50 ms */) - , mAudioDiffAvgCount(0) - { } - virtual ~MovieAudioDecoder() - { - av_freep(&mFrame); - } - - void getInfo(int *samplerate, MWSound::ChannelConfig *chans, MWSound::SampleType * type) - { - if(mAVStream->codec->sample_fmt == AV_SAMPLE_FMT_U8) - *type = MWSound::SampleType_UInt8; - else if(mAVStream->codec->sample_fmt == AV_SAMPLE_FMT_S16) - *type = MWSound::SampleType_Int16; - else if(mAVStream->codec->sample_fmt == AV_SAMPLE_FMT_FLT) - *type = MWSound::SampleType_Float32; - else - fail(std::string("Unsupported sample format: ")+ - av_get_sample_fmt_name(mAVStream->codec->sample_fmt)); - - if(mAVStream->codec->channel_layout == AV_CH_LAYOUT_MONO) - *chans = MWSound::ChannelConfig_Mono; - else if(mAVStream->codec->channel_layout == AV_CH_LAYOUT_STEREO) - *chans = MWSound::ChannelConfig_Stereo; - else if(mAVStream->codec->channel_layout == AV_CH_LAYOUT_QUAD) - *chans = MWSound::ChannelConfig_Quad; - else if(mAVStream->codec->channel_layout == AV_CH_LAYOUT_5POINT1) - *chans = MWSound::ChannelConfig_5point1; - else if(mAVStream->codec->channel_layout == AV_CH_LAYOUT_7POINT1) - *chans = MWSound::ChannelConfig_7point1; - else if(mAVStream->codec->channel_layout == 0) - { - /* Unknown channel layout. Try to guess. */ - if(mAVStream->codec->channels == 1) - *chans = MWSound::ChannelConfig_Mono; - else if(mAVStream->codec->channels == 2) - *chans = MWSound::ChannelConfig_Stereo; - else - { - std::stringstream sstr("Unsupported raw channel count: "); - sstr << mAVStream->codec->channels; - fail(sstr.str()); - } - } - else - { - char str[1024]; - av_get_channel_layout_string(str, sizeof(str), mAVStream->codec->channels, - mAVStream->codec->channel_layout); - fail(std::string("Unsupported channel layout: ")+str); - } - - *samplerate = mAVStream->codec->sample_rate; - } - - size_t read(char *stream, size_t len) - { - int sample_skip = synchronize_audio(); - size_t total = 0; - - while(total < len) - { - if(mFramePos >= mFrameSize) - { - /* We have already sent all our data; get more */ - mFrameSize = audio_decode_frame(mFrame); - if(mFrameSize < 0) - { - /* If error, we're done */ - break; - } - - mFramePos = std::min(mFrameSize, sample_skip); - sample_skip -= mFramePos; - continue; - } - - size_t len1 = len - total; - if(mFramePos >= 0) - { - len1 = std::min(len1, mFrameSize-mFramePos); - memcpy(stream, mFrame->data[0]+mFramePos, len1); - } - else - { - len1 = std::min(len1, -mFramePos); - - int n = av_get_bytes_per_sample(mAVStream->codec->sample_fmt) * - mAVStream->codec->channels; - - /* add samples by copying the first sample*/ - if(n == 1) - memset(stream, *mFrame->data[0], len1); - else if(n == 2) - { - const int16_t val = *((int16_t*)mFrame->data[0]); - for(size_t nb = 0;nb < len1;nb += n) - *((int16_t*)(stream+nb)) = val; - } - else if(n == 4) - { - const int32_t val = *((int32_t*)mFrame->data[0]); - for(size_t nb = 0;nb < len1;nb += n) - *((int32_t*)(stream+nb)) = val; - } - else if(n == 8) - { - const int64_t val = *((int64_t*)mFrame->data[0]); - for(size_t nb = 0;nb < len1;nb += n) - *((int64_t*)(stream+nb)) = val; - } - else - { - for(size_t nb = 0;nb < len1;nb += n) - memcpy(stream+nb, mFrame->data[0], n); - } - } - - total += len1; - stream += len1; - mFramePos += len1; - } - - return total; - } - - size_t getSampleOffset() - { - ssize_t clock_delay = (mFrameSize-mFramePos) / mAVStream->codec->channels / - av_get_bytes_per_sample(mAVStream->codec->sample_fmt); - return (size_t)(mAudioClock*mAVStream->codec->sample_rate) - clock_delay; - } -}; - - -int VideoState::OgreResource_Read(void *user_data, uint8_t *buf, int buf_size) -{ - Ogre::DataStreamPtr stream = static_cast(user_data)->stream; - return stream->read(buf, buf_size); -} - -int VideoState::OgreResource_Write(void *user_data, uint8_t *buf, int buf_size) -{ - Ogre::DataStreamPtr stream = static_cast(user_data)->stream; - return stream->write(buf, buf_size); -} - -int64_t VideoState::OgreResource_Seek(void *user_data, int64_t offset, int whence) -{ - Ogre::DataStreamPtr stream = static_cast(user_data)->stream; - - whence &= ~AVSEEK_FORCE; - if(whence == AVSEEK_SIZE) - return stream->size(); - if(whence == SEEK_SET) - stream->seek(offset); - else if(whence == SEEK_CUR) - stream->seek(stream->tell()+offset); - else if(whence == SEEK_END) - stream->seek(stream->size()+offset); - else - return -1; - - return stream->tell(); -} - - -void VideoState::video_refresh(VideoState* is) -{ - boost::system_time t = boost::get_system_time(); - while(!is->quit) - { - t += boost::posix_time::milliseconds(is->refresh_rate_ms); - boost::this_thread::sleep(t); - is->refresh = true; - } -} - - -void VideoState::video_display() -{ - VideoPicture *vp = &this->pictq[this->pictq_rindex]; - - if((*this->video_st)->codec->width != 0 && (*this->video_st)->codec->height != 0) - { - - if(static_cast(mTexture->getWidth()) != (*this->video_st)->codec->width || - static_cast(mTexture->getHeight()) != (*this->video_st)->codec->height) - { - mTexture->unload(); - mTexture->setWidth((*this->video_st)->codec->width); - mTexture->setHeight((*this->video_st)->codec->height); - mTexture->createInternalResources(); - } - Ogre::PixelBox pb((*this->video_st)->codec->width, (*this->video_st)->codec->height, 1, Ogre::PF_BYTE_RGBA, &vp->data[0]); - Ogre::HardwarePixelBufferSharedPtr buffer = mTexture->getBuffer(); - buffer->blitFromMemory(pb); - this->display_ready = true; - } -} - -void VideoState::video_refresh_timer() -{ - VideoPicture *vp; - double delay; - - if(this->pictq_size == 0) - return; - - vp = &this->pictq[this->pictq_rindex]; - - delay = vp->pts - this->frame_last_pts; /* the pts from last time */ - if(delay <= 0 || delay >= 1.0) { - /* if incorrect delay, use previous one */ - delay = this->frame_last_delay; - } - /* save for next time */ - this->frame_last_delay = delay; - this->frame_last_pts = vp->pts; - - /* FIXME: Syncing should be done in the decoding stage, where frames can be - * skipped or duplicated as needed. */ - /* update delay to sync to audio if not master source */ - if(this->av_sync_type != AV_SYNC_VIDEO_MASTER) - { - double diff = this->get_video_clock() - this->get_master_clock(); - - /* Skip or repeat the frame. Take delay into account - * FFPlay still doesn't "know if this is the best guess." */ - double sync_threshold = std::max(delay, AV_SYNC_THRESHOLD); - if(diff <= -sync_threshold) - delay = 0; - else if(diff >= sync_threshold) - delay = 2 * delay; - } - - this->refresh_rate_ms = std::max(1, (int)(delay*1000.0)); - /* show the picture! */ - this->video_display(); - - /* update queue for next picture! */ - this->pictq_rindex = (this->pictq_rindex+1) % VIDEO_PICTURE_QUEUE_SIZE; - this->pictq_mutex.lock(); - this->pictq_size--; - this->pictq_cond.notify_one(); - this->pictq_mutex.unlock(); -} - - -int VideoState::queue_picture(AVFrame *pFrame, double pts) -{ - VideoPicture *vp; - - /* wait until we have a new pic */ - { - boost::unique_lock lock(this->pictq_mutex); - while(this->pictq_size >= VIDEO_PICTURE_QUEUE_SIZE && !this->quit) - this->pictq_cond.timed_wait(lock, boost::posix_time::milliseconds(1)); - } - if(this->quit) - return -1; - - // windex is set to 0 initially - vp = &this->pictq[this->pictq_windex]; - - // Convert the image into RGBA format for Ogre - if(this->sws_context == NULL) - { - int w = (*this->video_st)->codec->width; - int h = (*this->video_st)->codec->height; - this->sws_context = sws_getContext(w, h, (*this->video_st)->codec->pix_fmt, - w, h, PIX_FMT_RGBA, SWS_BICUBIC, - NULL, NULL, NULL); - if(this->sws_context == NULL) - throw std::runtime_error("Cannot initialize the conversion context!\n"); - } - - vp->pts = pts; - vp->data.resize((*this->video_st)->codec->width * (*this->video_st)->codec->height * 4); - - uint8_t *dst = &vp->data[0]; - sws_scale(this->sws_context, pFrame->data, pFrame->linesize, - 0, (*this->video_st)->codec->height, &dst, this->rgbaFrame->linesize); - - // now we inform our display thread that we have a pic ready - this->pictq_windex = (this->pictq_windex+1) % VIDEO_PICTURE_QUEUE_SIZE; - this->pictq_mutex.lock(); - this->pictq_size++; - this->pictq_mutex.unlock(); - - return 0; -} - -double VideoState::synchronize_video(AVFrame *src_frame, double pts) -{ - double frame_delay; - - /* if we have pts, set video clock to it */ - if(pts != 0) - this->video_clock = pts; - else - pts = this->video_clock; - - /* update the video clock */ - frame_delay = av_q2d((*this->video_st)->codec->time_base); - - /* if we are repeating a frame, adjust clock accordingly */ - frame_delay += src_frame->repeat_pict * (frame_delay * 0.5); - this->video_clock += frame_delay; - - return pts; -} - - -/* These are called whenever we allocate a frame - * buffer. We use this to store the global_pts in - * a frame at the time it is allocated. - */ -static uint64_t global_video_pkt_pts = static_cast(AV_NOPTS_VALUE); -static int our_get_buffer(struct AVCodecContext *c, AVFrame *pic) -{ - int ret = avcodec_default_get_buffer(c, pic); - uint64_t *pts = (uint64_t*)av_malloc(sizeof(uint64_t)); - *pts = global_video_pkt_pts; - pic->opaque = pts; - return ret; -} -static void our_release_buffer(struct AVCodecContext *c, AVFrame *pic) -{ - if(pic) av_freep(&pic->opaque); - avcodec_default_release_buffer(c, pic); -} - - -void VideoState::video_thread_loop(VideoState *self) -{ - AVPacket pkt1, *packet = &pkt1; - int frameFinished; - AVFrame *pFrame; - double pts; - - pFrame = avcodec_alloc_frame(); - - self->rgbaFrame = avcodec_alloc_frame(); - avpicture_alloc((AVPicture*)self->rgbaFrame, PIX_FMT_RGBA, (*self->video_st)->codec->width, (*self->video_st)->codec->height); - - while(self->videoq.get(packet, self) >= 0) - { - // Save global pts to be stored in pFrame - global_video_pkt_pts = packet->pts; - // Decode video frame - if(avcodec_decode_video2((*self->video_st)->codec, pFrame, &frameFinished, packet) < 0) - throw std::runtime_error("Error decoding video frame"); - - pts = 0; - if((uint64_t)packet->dts != AV_NOPTS_VALUE) - pts = packet->dts; - else if(pFrame->opaque && *(uint64_t*)pFrame->opaque != AV_NOPTS_VALUE) - pts = *(uint64_t*)pFrame->opaque; - pts *= av_q2d((*self->video_st)->time_base); - - av_free_packet(packet); - - // Did we get a video frame? - if(frameFinished) - { - pts = self->synchronize_video(pFrame, pts); - if(self->queue_picture(pFrame, pts) < 0) - break; - } - } - - av_free(pFrame); - - avpicture_free((AVPicture*)self->rgbaFrame); - av_free(self->rgbaFrame); -} - -void VideoState::decode_thread_loop(VideoState *self) -{ - AVFormatContext *pFormatCtx = self->format_ctx; - AVPacket pkt1, *packet = &pkt1; - - try - { - if(!self->video_st && !self->audio_st) - throw std::runtime_error("No streams to decode"); - - // main decode loop - while(!self->quit) - { - if((self->audio_st && self->audioq.size > MAX_AUDIOQ_SIZE) || - (self->video_st && self->videoq.size > MAX_VIDEOQ_SIZE)) - { - boost::this_thread::sleep(boost::posix_time::milliseconds(10)); - continue; - } - - if(av_read_frame(pFormatCtx, packet) < 0) - break; - - // Is this a packet from the video stream? - if(self->video_st && packet->stream_index == self->video_st-pFormatCtx->streams) - self->videoq.put(packet); - else if(self->audio_st && packet->stream_index == self->audio_st-pFormatCtx->streams) - self->audioq.put(packet); - else - av_free_packet(packet); - } - - /* all done - wait for it */ - self->videoq.flush(); - self->audioq.flush(); - while(!self->quit) - { - // EOF reached, all packets processed, we can exit now - if(self->audioq.nb_packets == 0 && self->videoq.nb_packets == 0) - break; - boost::this_thread::sleep(boost::posix_time::milliseconds(100)); - } - } - catch(std::runtime_error& e) { - std::cerr << "An error occured playing the video: " << e.what () << std::endl; - } - catch(Ogre::Exception& e) { - std::cerr << "An error occured playing the video: " << e.getFullDescription () << std::endl; - } - - self->quit = true; -} - - -bool VideoState::update() -{ - if(this->quit) - return false; - - if(this->refresh) - { - this->refresh = false; - this->video_refresh_timer(); - } - return true; -} - - -int VideoState::stream_open(int stream_index, AVFormatContext *pFormatCtx) -{ - MWSound::DecoderPtr decoder; - AVCodecContext *codecCtx; - AVCodec *codec; - - if(stream_index < 0 || stream_index >= static_cast(pFormatCtx->nb_streams)) - return -1; - - // Get a pointer to the codec context for the video stream - codecCtx = pFormatCtx->streams[stream_index]->codec; - codec = avcodec_find_decoder(codecCtx->codec_id); - if(!codec || (avcodec_open2(codecCtx, codec, NULL) < 0)) - { - fprintf(stderr, "Unsupported codec!\n"); - return -1; - } - - switch(codecCtx->codec_type) - { - case AVMEDIA_TYPE_AUDIO: - this->audio_st = pFormatCtx->streams + stream_index; - - decoder.reset(new MovieAudioDecoder(this)); - this->AudioTrack = MWBase::Environment::get().getSoundManager()->playTrack(decoder, MWBase::SoundManager::Play_TypeMovie); - if(!this->AudioTrack) - { - avcodec_close((*this->audio_st)->codec); - this->audio_st = NULL; - return -1; - } - break; - - case AVMEDIA_TYPE_VIDEO: - this->video_st = pFormatCtx->streams + stream_index; - - this->frame_last_delay = 40e-3; - - codecCtx->get_buffer = our_get_buffer; - codecCtx->release_buffer = our_release_buffer; - this->video_thread = boost::thread(video_thread_loop, this); - this->refresh_thread = boost::thread(video_refresh, this); - break; - - default: - break; - } - - return 0; -} - -void VideoState::init(const std::string& resourceName) -{ - int video_index = -1; - int audio_index = -1; - unsigned int i; - - this->av_sync_type = AV_SYNC_DEFAULT; - this->refresh_rate_ms = 10; - this->refresh = false; - this->quit = false; - - this->stream = Ogre::ResourceGroupManager::getSingleton().openResource(resourceName); - if(this->stream.isNull()) - throw std::runtime_error("Failed to open video resource"); - - AVIOContext *ioCtx = avio_alloc_context(NULL, 0, 0, this, OgreResource_Read, OgreResource_Write, OgreResource_Seek); - if(!ioCtx) throw std::runtime_error("Failed to allocate AVIOContext"); - - this->format_ctx = avformat_alloc_context(); - if(this->format_ctx) - this->format_ctx->pb = ioCtx; - - // Open video file - /// - /// format_ctx->pb->buffer must be freed by hand, - /// if not, valgrind will show memleak, see: - /// - /// https://trac.ffmpeg.org/ticket/1357 - /// - if(!this->format_ctx || avformat_open_input(&this->format_ctx, resourceName.c_str(), NULL, NULL)) - { - if (this->format_ctx != NULL) - { - if (this->format_ctx->pb != NULL) - { - av_free(this->format_ctx->pb->buffer); - this->format_ctx->pb->buffer = NULL; - - av_free(this->format_ctx->pb); - this->format_ctx->pb = NULL; - } - } - // "Note that a user-supplied AVFormatContext will be freed on failure." - this->format_ctx = NULL; - av_free(ioCtx); - throw std::runtime_error("Failed to open video input"); - } - - // Retrieve stream information - if(avformat_find_stream_info(this->format_ctx, NULL) < 0) - throw std::runtime_error("Failed to retrieve stream information"); - - // Dump information about file onto standard error - av_dump_format(this->format_ctx, 0, resourceName.c_str(), 0); - - for(i = 0;i < this->format_ctx->nb_streams;i++) - { - if(this->format_ctx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO && video_index < 0) - video_index = i; - if(this->format_ctx->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO && audio_index < 0) - audio_index = i; - } - - if (audio_index != -1) - MWBase::Environment::get().getSoundManager()->pauseSounds(); - - this->external_clock_base = av_gettime(); - -#if !defined(_WIN32) || defined(FFMPEG_PLAY_BINKAUDIO) - if(audio_index >= 0) - this->stream_open(audio_index, this->format_ctx); -#else - std::cout<<"FFmpeg sound disabled for \""+resourceName+"\""<= 0) - { - this->stream_open(video_index, this->format_ctx); - - int width = (*this->video_st)->codec->width; - int height = (*this->video_st)->codec->height; - static int i = 0; - this->mTexture = Ogre::TextureManager::getSingleton().createManual( - "OpenMW/VideoTexture" + Ogre::StringConverter::toString(++i), - Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, - Ogre::TEX_TYPE_2D, - width, height, // TEST - 0, - Ogre::PF_BYTE_RGBA, - Ogre::TU_DYNAMIC_WRITE_ONLY_DISCARDABLE); - - // initialize to (0,0,0,0) - std::vector buffer; - buffer.resize(width * height, 0); - Ogre::PixelBox pb(width, height, 1, Ogre::PF_BYTE_RGBA, &buffer[0]); - this->mTexture->getBuffer()->blitFromMemory(pb); - } - - - this->parse_thread = boost::thread(decode_thread_loop, this); -} - -void VideoState::deinit() -{ - this->quit = true; - - this->audioq.cond.notify_one(); - this->videoq.cond.notify_one(); - - if (this->parse_thread.joinable()) - this->parse_thread.join(); - if (this->video_thread.joinable()) - this->video_thread.join(); - if (this->refresh_thread.joinable()) - this->refresh_thread.join(); - - if(this->audio_st) - avcodec_close((*this->audio_st)->codec); - this->audio_st = NULL; - if(this->video_st) - avcodec_close((*this->video_st)->codec); - this->video_st = NULL; - - if(this->sws_context) - sws_freeContext(this->sws_context); - this->sws_context = NULL; - - if(this->format_ctx) - { - /// - /// format_ctx->pb->buffer must be freed by hand, - /// if not, valgrind will show memleak, see: - /// - /// https://trac.ffmpeg.org/ticket/1357 - /// - if (this->format_ctx->pb != NULL) - { - av_free(this->format_ctx->pb->buffer); - this->format_ctx->pb->buffer = NULL; - - av_free(this->format_ctx->pb); - this->format_ctx->pb = NULL;; - } - avformat_close_input(&this->format_ctx); - } -} - -#else // defined OPENMW_USE_FFMPEG - -class VideoState -{ -public: - VideoState() { } - - void init(const std::string& resourceName) - { - throw std::runtime_error("FFmpeg not supported, cannot play \""+resourceName+"\""); - } - void deinit() { } - - void close() { } - - bool update() - { return false; } -}; - -#endif // defined OPENMW_USE_FFMPEG - - -VideoPlayer::VideoPlayer() - : mState(NULL) -{ - -} - -VideoPlayer::~VideoPlayer() -{ - if(mState) - close(); -} - -void VideoPlayer::playVideo(const std::string &resourceName) -{ - if(mState) - close(); - - try { - mState = new VideoState; - mState->init(resourceName); - } - catch(std::exception& e) { - std::cerr<< "Failed to play video: "<update()) - close(); - } -} - -std::string VideoPlayer::getTextureName() -{ - std::string name; - if (mState) - name = mState->mTexture->getName(); - return name; -} - -int VideoPlayer::getVideoWidth() -{ - int width=0; - if (mState) - width = mState->mTexture->getWidth(); - return width; -} - -int VideoPlayer::getVideoHeight() -{ - int height=0; - if (mState) - height = mState->mTexture->getHeight(); - return height; -} - -void VideoPlayer::stopVideo () -{ - close(); -} - -void VideoPlayer::close() -{ - if(mState) - { - mState->deinit(); - - delete mState; - mState = NULL; - } - - MWBase::Environment::get().getSoundManager()->resumeSounds(); -} - -bool VideoPlayer::isPlaying () -{ - return mState != NULL; -} - -} diff --git a/apps/openmw/mwrender/videoplayer.hpp b/apps/openmw/mwrender/videoplayer.hpp deleted file mode 100644 index 47e252cc1..000000000 --- a/apps/openmw/mwrender/videoplayer.hpp +++ /dev/null @@ -1,41 +0,0 @@ -#ifndef VIDEOPLAYER_H -#define VIDEOPLAYER_H - -#include - -namespace MWRender -{ - struct VideoState; - - /** - * @brief Plays a video on an Ogre texture. - */ - class VideoPlayer - { - public: - VideoPlayer(); - ~VideoPlayer(); - - void playVideo (const std::string& resourceName); - - void update(); - - void close(); - void stopVideo(); - - bool isPlaying(); - - std::string getTextureName(); - int getVideoWidth(); - int getVideoHeight(); - - - private: - VideoState* mState; - - int mWidth; - int mHeight; - }; -} - -#endif diff --git a/apps/openmw/mwrender/water.cpp b/apps/openmw/mwrender/water.cpp index 2cbc4462c..456fc47e9 100644 --- a/apps/openmw/mwrender/water.cpp +++ b/apps/openmw/mwrender/water.cpp @@ -20,9 +20,6 @@ #include #include -#include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" - using namespace Ogre; namespace MWRender @@ -74,7 +71,8 @@ CubeReflection::~CubeReflection () void CubeReflection::update () { - mParentCamera->getParentSceneNode ()->needUpdate (); + if (mParentCamera->isAttached()) + mParentCamera->getParentSceneNode ()->needUpdate (); mCamera->setPosition(mParentCamera->getDerivedPosition()); } @@ -133,7 +131,8 @@ void PlaneReflection::renderQueueEnded (Ogre::uint8 queueGroupId, const Ogre::St void PlaneReflection::preRenderTargetUpdate(const Ogre::RenderTargetEvent& evt) { - mParentCamera->getParentSceneNode ()->needUpdate (); + if (mParentCamera->isAttached()) + mParentCamera->getParentSceneNode ()->needUpdate (); mCamera->setOrientation(mParentCamera->getDerivedOrientation()); mCamera->setPosition(mParentCamera->getDerivedPosition()); mCamera->setNearClipDistance(mParentCamera->getNearClipDistance()); @@ -185,7 +184,7 @@ void PlaneReflection::setVisibilityMask (int flags) // -------------------------------------------------------------------------------------------------------------------------------- -Water::Water (Ogre::Camera *camera, RenderingManager* rend) : +Water::Water (Ogre::Camera *camera, RenderingManager* rend, const MWWorld::Fallback* fallback) : mCamera (camera), mSceneMgr (camera->getSceneManager()), mIsUnderwater(false), mVisibilityFlags(0), mActive(1), mToggled(1), @@ -196,7 +195,7 @@ Water::Water (Ogre::Camera *camera, RenderingManager* rend) : mSimulation(NULL), mPlayer(0,0) { - mSimulation = new RippleSimulation(mSceneMgr); + mSimulation = new RippleSimulation(mSceneMgr, fallback); mSky = rend->getSkyManager(); @@ -208,10 +207,10 @@ Water::Water (Ogre::Camera *camera, RenderingManager* rend) : mWaterPlane = Plane(Vector3::UNIT_Z, 0); - int waterScale = 300; + int waterScale = 30; MeshManager::getSingleton().createPlane("water", ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, mWaterPlane, - CELL_SIZE*5*waterScale, CELL_SIZE*5*waterScale, 10, 10, true, 1, 3*waterScale,3*waterScale, Vector3::UNIT_Y); + CELL_SIZE*5*waterScale, CELL_SIZE*5*waterScale, 40, 40, true, 1, 3*waterScale,3*waterScale, Vector3::UNIT_Y); mWater = mSceneMgr->createEntity("water"); mWater->setVisibilityFlags(RV_Water); @@ -303,11 +302,7 @@ Water::~Water() void Water::changeCell(const ESM::Cell* cell) { - mTop = cell->mWater; - - setHeight(mTop); - - if(!(cell->mData.mFlags & cell->Interior)) + if(cell->isExterior()) mWaterNode->setPosition(getSceneNodeCoordinates(cell->mData.mX, cell->mData.mY)); } @@ -315,6 +310,8 @@ void Water::setHeight(const float height) { mTop = height; + mSimulation->setWaterHeight(height); + mWaterPlane = Plane(Vector3::UNIT_Z, -height); if (mReflection) @@ -384,10 +381,15 @@ void Water::update(float dt, Ogre::Vector3 player) void Water::frameStarted(float dt) { + if (!mActive) + return; + mSimulation->update(dt, mPlayer); if (mReflection) + { mReflection->update(); + } } void Water::applyRTT() @@ -421,6 +423,7 @@ void Water::applyVisibilityMask() mVisibilityFlags = RV_Terrain * Settings::Manager::getBool("reflect terrain", "Water") + (RV_Statics + RV_StaticsSmall + RV_Misc) * Settings::Manager::getBool("reflect statics", "Water") + RV_Actors * Settings::Manager::getBool("reflect actors", "Water") + + RV_Effects + RV_Sky; if (mReflection) @@ -497,4 +500,9 @@ void Water::updateEmitterPtr (const MWWorld::Ptr& old, const MWWorld::Ptr& ptr) mSimulation->updateEmitterPtr(old, ptr); } +void Water::clearRipples() +{ + mSimulation->clear(); +} + } // namespace diff --git a/apps/openmw/mwrender/water.hpp b/apps/openmw/mwrender/water.hpp index 6a7b05a3a..10d0a06ff 100644 --- a/apps/openmw/mwrender/water.hpp +++ b/apps/openmw/mwrender/water.hpp @@ -30,6 +30,11 @@ namespace Ogre struct RenderTargetEvent; } +namespace MWWorld +{ + class Fallback; +} + namespace MWRender { class SkyManager; @@ -145,9 +150,11 @@ namespace MWRender { Ogre::Vector2 mPlayer; public: - Water (Ogre::Camera *camera, RenderingManager* rend); + Water (Ogre::Camera *camera, RenderingManager* rend, const MWWorld::Fallback* fallback); ~Water(); + void clearRipples(); + void setActive(bool active); bool toggle(); diff --git a/apps/openmw/mwrender/weaponanimation.cpp b/apps/openmw/mwrender/weaponanimation.cpp index cd9ae16b8..d16afe3ce 100644 --- a/apps/openmw/mwrender/weaponanimation.cpp +++ b/apps/openmw/mwrender/weaponanimation.cpp @@ -7,12 +7,14 @@ #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" +#include "../mwbase/soundmanager.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwmechanics/creaturestats.hpp" +#include "../mwmechanics/combat.hpp" #include "animation.hpp" @@ -44,18 +46,37 @@ void WeaponAnimation::attachArrow(MWWorld::Ptr actor) { MWWorld::InventoryStore& inv = actor.getClass().getInventoryStore(actor); MWWorld::ContainerStoreIterator weaponSlot = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - if (weaponSlot != inv.end() && weaponSlot->get()->mBase->mData.mType == ESM::Weapon::MarksmanThrown) + if (weaponSlot == inv.end()) + return; + if (weaponSlot->getTypeName() != typeid(ESM::Weapon).name()) + return; + int weaponType = weaponSlot->get()->mBase->mData.mType; + if (weaponType == ESM::Weapon::MarksmanThrown) + { + std::string soundid = weaponSlot->getClass().getUpSoundId(*weaponSlot); + if(!soundid.empty()) + { + MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); + sndMgr->playSound3D(actor, soundid, 1.0f, 1.0f); + } showWeapon(true); - else + } + else if (weaponType == ESM::Weapon::MarksmanBow || weaponType == ESM::Weapon::MarksmanCrossbow) { NifOgre::ObjectScenePtr weapon = getWeapon(); + if (!weapon.get()) + return; MWWorld::ContainerStoreIterator ammo = inv.getSlot(MWWorld::InventoryStore::Slot_Ammunition); if (ammo == inv.end()) return; std::string model = ammo->getClass().getModel(*ammo); - mAmmunition = NifOgre::Loader::createObjects(weapon->mSkelBase, "ArrowBone", weapon->mSkelBase->getParentSceneNode(), model); + if (!weapon->mSkelBase) + throw std::runtime_error("Need a skeleton to attach the arrow to"); + + const std::string bonename = "ArrowBone"; + mAmmunition = NifOgre::Loader::createObjects(weapon->mSkelBase, bonename, bonename, weapon->mSkelBase->getParentSceneNode(), model); configureAddedObject(mAmmunition, *ammo, MWWorld::InventoryStore::Slot_Ammunition); } } @@ -66,6 +87,8 @@ void WeaponAnimation::releaseArrow(MWWorld::Ptr actor) MWWorld::ContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); if (weapon == inv.end()) return; + if (weapon->getTypeName() != typeid(ESM::Weapon).name()) + return; // The orientation of the launched projectile. Always the same as the actor orientation, even if the ArrowBone's orientation dictates otherwise. Ogre::Quaternion orient = Ogre::Quaternion(Ogre::Radian(actor.getRefData().getPosition().rot[2]), Ogre::Vector3::NEGATIVE_UNIT_Z) * @@ -74,19 +97,7 @@ void WeaponAnimation::releaseArrow(MWWorld::Ptr actor) const MWWorld::Store &gmst = MWBase::Environment::get().getWorld()->getStore().get(); - // Reduce fatigue - // somewhat of a guess, but using the weapon weight makes sense - const float fFatigueAttackBase = gmst.find("fFatigueAttackBase")->getFloat(); - const float fFatigueAttackMult = gmst.find("fFatigueAttackMult")->getFloat(); - const float fWeaponFatigueMult = gmst.find("fWeaponFatigueMult")->getFloat(); - MWMechanics::CreatureStats& attackerStats = actor.getClass().getCreatureStats(actor); - MWMechanics::DynamicStat fatigue = attackerStats.getFatigue(); - const float normalizedEncumbrance = actor.getClass().getEncumbrance(actor) / actor.getClass().getCapacity(actor); - float fatigueLoss = fFatigueAttackBase + normalizedEncumbrance * fFatigueAttackMult; - if (!weapon->isEmpty()) - fatigueLoss += weapon->getClass().getWeight(*weapon) * attackerStats.getAttackStrength() * fWeaponFatigueMult; - fatigue.setCurrent(fatigue.getCurrent() - fatigueLoss); - attackerStats.setFatigue(fatigue); + MWMechanics::applyFatigueLoss(actor, *weapon); if (weapon->get()->mBase->mData.mType == ESM::Weapon::MarksmanThrown) { @@ -122,6 +133,9 @@ void WeaponAnimation::releaseArrow(MWWorld::Ptr actor) if (ammo == inv.end()) return; + if (!mAmmunition.get()) + return; + Ogre::Vector3 launchPos(0,0,0); if (mAmmunition->mSkelBase) { diff --git a/apps/openmw/mwrender/weaponanimation.hpp b/apps/openmw/mwrender/weaponanimation.hpp index cbe910c71..e1ccd9465 100644 --- a/apps/openmw/mwrender/weaponanimation.hpp +++ b/apps/openmw/mwrender/weaponanimation.hpp @@ -35,8 +35,11 @@ namespace MWRender WeaponAnimation() : mPitchFactor(0) {} virtual ~WeaponAnimation() {} - virtual void attachArrow(MWWorld::Ptr actor); - virtual void releaseArrow(MWWorld::Ptr actor); + /// @note If no weapon (or an invalid weapon) is equipped, this function is a no-op. + void attachArrow(MWWorld::Ptr actor); + + /// @note If no weapon (or an invalid weapon) is equipped, this function is a no-op. + void releaseArrow(MWWorld::Ptr actor); protected: NifOgre::ObjectScenePtr mAmmunition; diff --git a/apps/openmw/mwscript/aiextensions.cpp b/apps/openmw/mwscript/aiextensions.cpp index d844138d6..a3f935487 100644 --- a/apps/openmw/mwscript/aiextensions.cpp +++ b/apps/openmw/mwscript/aiextensions.cpp @@ -420,7 +420,6 @@ namespace MWScript runtime.pop(); const MWMechanics::CreatureStats& creatureStats = actor.getClass().getCreatureStats(actor); - std::string currentTargetId; bool targetsAreEqual = false; MWWorld::Ptr targetPtr; @@ -457,7 +456,6 @@ namespace MWScript MWWorld::Ptr actor = R()(runtime); MWMechanics::CreatureStats& creatureStats = actor.getClass().getCreatureStats(actor); creatureStats.getAiSequence().stopCombat(); - creatureStats.setHostile(false); } }; diff --git a/apps/openmw/mwscript/animationextensions.cpp b/apps/openmw/mwscript/animationextensions.cpp index 52de8e042..c43cdf565 100644 --- a/apps/openmw/mwscript/animationextensions.cpp +++ b/apps/openmw/mwscript/animationextensions.cpp @@ -2,6 +2,7 @@ #include "animationextensions.hpp" #include +#include #include #include @@ -10,6 +11,7 @@ #include #include +#include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "interpretercontext.hpp" @@ -55,7 +57,7 @@ namespace MWScript throw std::runtime_error ("animation mode out of range"); } - MWBase::Environment::get().getMechanicsManager()->playAnimationGroup (ptr, group, mode, 1); + MWBase::Environment::get().getMechanicsManager()->playAnimationGroup (ptr, group, mode, std::numeric_limits::max()); } }; diff --git a/apps/openmw/mwscript/cellextensions.cpp b/apps/openmw/mwscript/cellextensions.cpp index 94d734029..43d213c5a 100644 --- a/apps/openmw/mwscript/cellextensions.cpp +++ b/apps/openmw/mwscript/cellextensions.cpp @@ -117,18 +117,9 @@ namespace MWScript runtime.push(0); return; } - const ESM::Cell *cell = MWBase::Environment::get().getWorld()->getPlayerPtr().getCell()->getCell(); + const MWWorld::CellStore *cell = MWBase::Environment::get().getWorld()->getPlayerPtr().getCell(); - std::string current = cell->mName; - - if (!(cell->mData.mFlags & ESM::Cell::Interior) && current.empty() - && !cell->mRegion.empty()) - { - const ESM::Region *region = - MWBase::Environment::get().getWorld()->getStore().get().find (cell->mRegion); - - current = region->mName; - } + std::string current = MWBase::Environment::get().getWorld()->getCellName(cell); Misc::StringUtils::toLower(current); bool match = current.length()>=name.length() && @@ -153,7 +144,7 @@ namespace MWScript if (cell->getCell()->hasWater()) runtime.push (cell->getWaterLevel()); else - runtime.push (-std::numeric_limits().max()); + runtime.push (-std::numeric_limits::max()); } }; diff --git a/apps/openmw/mwscript/compilercontext.hpp b/apps/openmw/mwscript/compilercontext.hpp index 95719ab69..010926f45 100644 --- a/apps/openmw/mwscript/compilercontext.hpp +++ b/apps/openmw/mwscript/compilercontext.hpp @@ -12,7 +12,7 @@ namespace MWScript enum Type { Type_Full, // global, local, targetted - Type_Dialgoue, + Type_Dialogue, Type_Console }; diff --git a/apps/openmw/mwscript/containerextensions.cpp b/apps/openmw/mwscript/containerextensions.cpp index 93711d036..86329191e 100644 --- a/apps/openmw/mwscript/containerextensions.cpp +++ b/apps/openmw/mwscript/containerextensions.cpp @@ -14,10 +14,13 @@ #include #include +#include + #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" @@ -71,8 +74,7 @@ namespace MWScript msgBox = MyGUI::LanguageManager::getInstance().replaceTags("#{sNotifyMessage61}"); msgBox = boost::str(boost::format(msgBox) % count % itemName); } - std::vector noButtons; - MWBase::Environment::get().getWindowManager()->messageBox(msgBox, noButtons, MWGui::ShowInDialogueMode_Only); + MWBase::Environment::get().getWindowManager()->messageBox(msgBox, MWGui::ShowInDialogueMode_Only); } } }; @@ -143,8 +145,7 @@ namespace MWScript msgBox = MyGUI::LanguageManager::getInstance().replaceTags("#{sNotifyMessage62}"); msgBox = boost::str (boost::format(msgBox) % itemName); } - std::vector noButtons; - MWBase::Environment::get().getWindowManager()->messageBox(msgBox, noButtons, MWGui::ShowInDialogueMode_Only); + MWBase::Environment::get().getWindowManager()->messageBox(msgBox, MWGui::ShowInDialogueMode_Only); } } }; @@ -290,18 +291,15 @@ namespace MWScript const std::string &name = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); + int count = 0; MWWorld::InventoryStore& invStore = ptr.getClass().getInventoryStore (ptr); for (MWWorld::ContainerStoreIterator it = invStore.begin(MWWorld::ContainerStore::Type_Miscellaneous); it != invStore.end(); ++it) { - if (::Misc::StringUtils::ciEqual(it->getCellRef().getSoul(), name)) - { - runtime.push(1); - return; - } + ++count; } - runtime.push(0); + runtime.push(count); } }; diff --git a/apps/openmw/mwscript/controlextensions.cpp b/apps/openmw/mwscript/controlextensions.cpp index d2e774859..fd7fe4737 100644 --- a/apps/openmw/mwscript/controlextensions.cpp +++ b/apps/openmw/mwscript/controlextensions.cpp @@ -10,6 +10,7 @@ #include "../mwbase/environment.hpp" #include "../mwbase/inputmanager.hpp" +#include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "../mwworld/ptr.hpp" @@ -121,6 +122,34 @@ namespace MWScript } }; + template + class OpGetForceJump : public Interpreter::Opcode0 + { + public: + + virtual void execute (Interpreter::Runtime& runtime) + { + MWWorld::Ptr ptr = R()(runtime); + + MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats (ptr); + runtime.push (stats.getMovementFlag (MWMechanics::CreatureStats::Flag_ForceJump)); + } + }; + + template + class OpGetForceMoveJump : public Interpreter::Opcode0 + { + public: + + virtual void execute (Interpreter::Runtime& runtime) + { + MWWorld::Ptr ptr = R()(runtime); + + MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats (ptr); + runtime.push (stats.getMovementFlag (MWMechanics::CreatureStats::Flag_ForceMoveJump)); + } + }; + template class OpGetForceSneak : public Interpreter::Opcode0 { @@ -169,27 +198,54 @@ namespace MWScript interpreter.installSegment5 (Compiler::Control::opcodeToggleCollision, new OpToggleCollision); + //Force Run interpreter.installSegment5 (Compiler::Control::opcodeClearForceRun, new OpClearMovementFlag (MWMechanics::CreatureStats::Flag_ForceRun)); - interpreter.installSegment5 (Compiler::Control::opcodeForceRun, - new OpSetMovementFlag (MWMechanics::CreatureStats::Flag_ForceRun)); - interpreter.installSegment5 (Compiler::Control::opcodeClearForceSneak, - new OpClearMovementFlag (MWMechanics::CreatureStats::Flag_ForceSneak)); - interpreter.installSegment5 (Compiler::Control::opcodeForceSneak, - new OpSetMovementFlag (MWMechanics::CreatureStats::Flag_ForceSneak)); - interpreter.installSegment5 (Compiler::Control::opcodeClearForceRunExplicit, new OpClearMovementFlag (MWMechanics::CreatureStats::Flag_ForceRun)); + interpreter.installSegment5 (Compiler::Control::opcodeForceRun, + new OpSetMovementFlag (MWMechanics::CreatureStats::Flag_ForceRun)); interpreter.installSegment5 (Compiler::Control::opcodeForceRunExplicit, new OpSetMovementFlag (MWMechanics::CreatureStats::Flag_ForceRun)); + + //Force Jump + interpreter.installSegment5 (Compiler::Control::opcodeClearForceJump, + new OpClearMovementFlag (MWMechanics::CreatureStats::Flag_ForceJump)); + interpreter.installSegment5 (Compiler::Control::opcodeClearForceJumpExplicit, + new OpClearMovementFlag (MWMechanics::CreatureStats::Flag_ForceJump)); + interpreter.installSegment5 (Compiler::Control::opcodeForceJump, + new OpSetMovementFlag (MWMechanics::CreatureStats::Flag_ForceJump)); + interpreter.installSegment5 (Compiler::Control::opcodeForceJumpExplicit, + new OpSetMovementFlag (MWMechanics::CreatureStats::Flag_ForceJump)); + + //Force MoveJump + interpreter.installSegment5 (Compiler::Control::opcodeClearForceMoveJump, + new OpClearMovementFlag (MWMechanics::CreatureStats::Flag_ForceMoveJump)); + interpreter.installSegment5 (Compiler::Control::opcodeClearForceMoveJumpExplicit, + new OpClearMovementFlag (MWMechanics::CreatureStats::Flag_ForceMoveJump)); + interpreter.installSegment5 (Compiler::Control::opcodeForceMoveJump, + new OpSetMovementFlag (MWMechanics::CreatureStats::Flag_ForceMoveJump)); + interpreter.installSegment5 (Compiler::Control::opcodeForceMoveJumpExplicit, + new OpSetMovementFlag (MWMechanics::CreatureStats::Flag_ForceMoveJump)); + + //Force Sneak + interpreter.installSegment5 (Compiler::Control::opcodeClearForceSneak, + new OpClearMovementFlag (MWMechanics::CreatureStats::Flag_ForceSneak)); interpreter.installSegment5 (Compiler::Control::opcodeClearForceSneakExplicit, new OpClearMovementFlag (MWMechanics::CreatureStats::Flag_ForceSneak)); + interpreter.installSegment5 (Compiler::Control::opcodeForceSneak, + new OpSetMovementFlag (MWMechanics::CreatureStats::Flag_ForceSneak)); interpreter.installSegment5 (Compiler::Control::opcodeForceSneakExplicit, new OpSetMovementFlag (MWMechanics::CreatureStats::Flag_ForceSneak)); + interpreter.installSegment5 (Compiler::Control::opcodeGetPcRunning, new OpGetPcRunning); interpreter.installSegment5 (Compiler::Control::opcodeGetPcSneaking, new OpGetPcSneaking); interpreter.installSegment5 (Compiler::Control::opcodeGetForceRun, new OpGetForceRun); interpreter.installSegment5 (Compiler::Control::opcodeGetForceRunExplicit, new OpGetForceRun); + interpreter.installSegment5 (Compiler::Control::opcodeGetForceJump, new OpGetForceJump); + interpreter.installSegment5 (Compiler::Control::opcodeGetForceJumpExplicit, new OpGetForceJump); + interpreter.installSegment5 (Compiler::Control::opcodeGetForceMoveJump, new OpGetForceMoveJump); + interpreter.installSegment5 (Compiler::Control::opcodeGetForceMoveJumpExplicit, new OpGetForceMoveJump); interpreter.installSegment5 (Compiler::Control::opcodeGetForceSneak, new OpGetForceSneak); interpreter.installSegment5 (Compiler::Control::opcodeGetForceSneakExplicit, new OpGetForceSneak); } diff --git a/apps/openmw/mwscript/dialogueextensions.cpp b/apps/openmw/mwscript/dialogueextensions.cpp index ecaf4253e..8b6805264 100644 --- a/apps/openmw/mwscript/dialogueextensions.cpp +++ b/apps/openmw/mwscript/dialogueextensions.cpp @@ -11,6 +11,7 @@ #include "../mwbase/environment.hpp" #include "../mwbase/dialoguemanager.hpp" #include "../mwbase/journal.hpp" +#include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "../mwmechanics/npcstats.hpp" @@ -41,7 +42,8 @@ namespace MWScript } catch (...) { - MWBase::Environment::get().getJournal()->setJournalIndex(quest, index); + if (MWBase::Environment::get().getJournal()->getJournalIndex(quest) < index) + MWBase::Environment::get().getJournal()->setJournalIndex(quest, index); } } }; @@ -124,6 +126,9 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); + if (!ptr.getRefData().isEnabled()) + return; + MWBase::Environment::get().getDialogueManager()->startDialogue (ptr); } }; @@ -192,7 +197,7 @@ namespace MWScript MWWorld::Ptr player = MWBase::Environment::get().getWorld ()->getPlayerPtr(); - runtime.push (ptr.getClass().getNpcStats (ptr).isSameFaction (player.getClass().getNpcStats (player))); + player.getClass().getNpcStats (player).isInFaction(ptr.getClass().getPrimaryFaction(ptr)); } }; @@ -232,6 +237,25 @@ namespace MWScript } }; + class OpSetFactionReaction : public Interpreter::Opcode0 + { + public: + + virtual void execute (Interpreter::Runtime& runtime) + { + std::string faction1 = runtime.getStringLiteral (runtime[0].mInteger); + runtime.pop(); + + std::string faction2 = runtime.getStringLiteral (runtime[0].mInteger); + runtime.pop(); + + int newValue = runtime[0].mInteger; + runtime.pop(); + + MWBase::Environment::get().getDialogueManager()->setFactionReaction(faction1, faction2, newValue); + } + }; + template class OpClearInfoActor : public Interpreter::Opcode0 { @@ -264,6 +288,7 @@ namespace MWScript interpreter.installSegment5 (Compiler::Dialogue::opcodeSameFaction, new OpSameFaction); interpreter.installSegment5 (Compiler::Dialogue::opcodeSameFactionExplicit, new OpSameFaction); interpreter.installSegment5 (Compiler::Dialogue::opcodeModFactionReaction, new OpModFactionReaction); + interpreter.installSegment5 (Compiler::Dialogue::opcodeSetFactionReaction, new OpSetFactionReaction); interpreter.installSegment5 (Compiler::Dialogue::opcodeGetFactionReaction, new OpGetFactionReaction); interpreter.installSegment5 (Compiler::Dialogue::opcodeClearInfoActor, new OpClearInfoActor); interpreter.installSegment5 (Compiler::Dialogue::opcodeClearInfoActorExplicit, new OpClearInfoActor); diff --git a/apps/openmw/mwscript/docs/vmformat.txt b/apps/openmw/mwscript/docs/vmformat.txt index 8c9d6e480..93720aef6 100644 --- a/apps/openmw/mwscript/docs/vmformat.txt +++ b/apps/openmw/mwscript/docs/vmformat.txt @@ -405,5 +405,45 @@ op 0x200024c: Face op 0x200024d: Face, explicit op 0x200024e: GetStat (dummy function) op 0x200024f: GetStat (dummy function), explicit +op 0x2000250: GetCollidingPC +op 0x2000251: GetCollidingPC, explicit +op 0x2000252: GetCollidingActor +op 0x2000253: GetCollidingActor, explicit +op 0x2000254: HurtStandingActor +op 0x2000255: HurtStandingActor, explicit +op 0x2000256: HurtCollidingActor +op 0x2000257: HurtCollidingActor, explicit +op 0x2000258: ClearForceJump +op 0x2000259: ClearForceJump, explicit reference +op 0x200025a: ForceJump +op 0x200025b: ForceJump, explicit reference +op 0x200025c: ClearForceMoveJump +op 0x200025d: ClearForceMoveJump, explicit reference +op 0x200025e: ForceMoveJump +op 0x200025f: ForceMoveJump, explicit reference +op 0x2000260: GetForceJump +op 0x2000261: GetForceJump, explicit reference +op 0x2000262: GetForceMoveJump +op 0x2000263: GetForceMoveJump, explicit reference +op 0x2000264-0x200027b: GetMagicEffect +op 0x200027c-0x2000293: GetMagicEffect, explicit +op 0x2000294-0x20002ab: SetMagicEffect +op 0x20002ac-0x20002c3: SetMagicEffect, explicit +op 0x20002c4-0x20002db: ModMagicEffect +op 0x20002dc-0x20002f3: ModMagicEffect, explicit +op 0x20002f4: ResetActors +op 0x20002f5: ToggleWorld +op 0x20002f6: PCForce1stPerson +op 0x20002f7: PCForce3rdPerson +op 0x20002f8: PCGet3rdPerson +op 0x20002f9: HitAttemptOnMe +op 0x20002fa: HitAttemptOnMe, explicit +op 0x20002fb: AddToLevCreature +op 0x20002fc: RemoveFromLevCreature +op 0x20002fd: AddToLevItem +op 0x20002fe: RemoveFromLevItem +op 0x20002ff: SetFactionReaction +op 0x2000300: EnableLevelupMenu +op 0x2000301: ToggleScripts -opcodes 0x2000250-0x3ffffff unused +opcodes 0x2000302-0x3ffffff unused diff --git a/apps/openmw/mwscript/globalscripts.cpp b/apps/openmw/mwscript/globalscripts.cpp index 8e118e2f8..a6ad2cc11 100644 --- a/apps/openmw/mwscript/globalscripts.cpp +++ b/apps/openmw/mwscript/globalscripts.cpp @@ -2,6 +2,7 @@ #include "globalscripts.hpp" #include +#include #include #include @@ -15,61 +16,69 @@ namespace MWScript { + GlobalScriptDesc::GlobalScriptDesc() : mRunning (false) {} + + GlobalScripts::GlobalScripts (const MWWorld::ESMStore& store) : mStore (store) - { - addStartup(); - } + {} - void GlobalScripts::addScript (const std::string& name) + void GlobalScripts::addScript (const std::string& name, const std::string& targetId) { - std::map >::iterator iter = + std::map::iterator iter = mScripts.find (::Misc::StringUtils::lowerCase (name)); if (iter==mScripts.end()) { if (const ESM::Script *script = mStore.get().find (name)) { - Locals locals; - - locals.configure (*script); + GlobalScriptDesc desc; + desc.mRunning = true; + desc.mLocals.configure (*script); + desc.mId = targetId; - mScripts.insert (std::make_pair (name, std::make_pair (true, locals))); + mScripts.insert (std::make_pair (name, desc)); } } - else - iter->second.first = true; + else if (!iter->second.mRunning) + { + iter->second.mRunning = true; + iter->second.mId = targetId; + } } void GlobalScripts::removeScript (const std::string& name) { - std::map >::iterator iter = + std::map::iterator iter = mScripts.find (::Misc::StringUtils::lowerCase (name)); if (iter!=mScripts.end()) - iter->second.first = false; + iter->second.mRunning = false; } bool GlobalScripts::isRunning (const std::string& name) const { - std::map >::const_iterator iter = + std::map::const_iterator iter = mScripts.find (::Misc::StringUtils::lowerCase (name)); if (iter==mScripts.end()) return false; - return iter->second.first; + return iter->second.mRunning; } void GlobalScripts::run() { - for (std::map >::iterator iter (mScripts.begin()); + for (std::map::iterator iter (mScripts.begin()); iter!=mScripts.end(); ++iter) { - if (iter->second.first) + if (iter->second.mRunning) { + MWWorld::Ptr ptr; + MWScript::InterpreterContext interpreterContext ( - &iter->second.second, MWWorld::Ptr()); + &iter->second.mLocals, MWWorld::Ptr(), iter->second.mId); + MWBase::Environment::get().getScriptManager()->run (iter->first, interpreterContext); } } @@ -82,13 +91,32 @@ namespace MWScript void GlobalScripts::addStartup() { - addScript ("main"); + // make list of global scripts to be added + std::vector scripts; + + scripts.push_back ("main"); for (MWWorld::Store::iterator iter = mStore.get().begin(); iter != mStore.get().end(); ++iter) { - addScript (iter->mScript); + scripts.push_back (iter->mId); + } + + // add scripts + for (std::vector::const_iterator iter (scripts.begin()); + iter!=scripts.end(); ++iter) + { + try + { + addScript (*iter); + } + catch (const std::exception& exception) + { + std::cerr + << "Failed to add start script " << *iter << " because an exception has " + << "been thrown: " << exception.what() << std::endl; + } } } @@ -99,50 +127,63 @@ namespace MWScript void GlobalScripts::write (ESM::ESMWriter& writer, Loading::Listener& progress) const { - for (std::map >::const_iterator iter (mScripts.begin()); + for (std::map::const_iterator iter (mScripts.begin()); iter!=mScripts.end(); ++iter) { ESM::GlobalScript script; script.mId = iter->first; - iter->second.second.write (script.mLocals, iter->first); + iter->second.mLocals.write (script.mLocals, iter->first); + + script.mRunning = iter->second.mRunning ? 1 : 0; - script.mRunning = iter->second.first ? 1 : 0; + script.mTargetId = iter->second.mId; writer.startRecord (ESM::REC_GSCR); script.save (writer); writer.endRecord (ESM::REC_GSCR); - progress.increaseProgress(); } } - bool GlobalScripts::readRecord (ESM::ESMReader& reader, int32_t type) + bool GlobalScripts::readRecord (ESM::ESMReader& reader, uint32_t type) { if (type==ESM::REC_GSCR) { ESM::GlobalScript script; script.load (reader); - std::map >::iterator iter = + std::map::iterator iter = mScripts.find (script.mId); if (iter==mScripts.end()) { if (const ESM::Script *scriptRecord = mStore.get().search (script.mId)) { - std::pair data (false, Locals()); - - data.second.configure (*scriptRecord); - - iter = mScripts.insert (std::make_pair (script.mId, data)).first; + try + { + GlobalScriptDesc desc; + desc.mLocals.configure (*scriptRecord); + + iter = mScripts.insert (std::make_pair (script.mId, desc)).first; + } + catch (const std::exception& exception) + { + std::cerr + << "Failed to add start script " << script.mId + << " because an exception has been thrown: " << exception.what() + << std::endl; + + return true; + } } else // script does not exist anymore return true; } - iter->second.first = script.mRunning!=0; - iter->second.second.read (script.mLocals, script.mId); + iter->second.mRunning = script.mRunning!=0; + iter->second.mLocals.read (script.mLocals, script.mId); + iter->second.mId = script.mTargetId; return true; } @@ -153,21 +194,19 @@ namespace MWScript Locals& GlobalScripts::getLocals (const std::string& name) { std::string name2 = ::Misc::StringUtils::lowerCase (name); - std::map >::iterator iter = - mScripts.find (name2); + std::map::iterator iter = mScripts.find (name2); if (iter==mScripts.end()) { if (const ESM::Script *script = mStore.get().find (name)) { - Locals locals; - - locals.configure (*script); + GlobalScriptDesc desc; + desc.mLocals.configure (*script); - iter = mScripts.insert (std::make_pair (name, std::make_pair (false, locals))).first; + iter = mScripts.insert (std::make_pair (name, desc)).first; } } - return iter->second.second; + return iter->second.mLocals; } } diff --git a/apps/openmw/mwscript/globalscripts.hpp b/apps/openmw/mwscript/globalscripts.hpp index 97584a5b8..9f009db98 100644 --- a/apps/openmw/mwscript/globalscripts.hpp +++ b/apps/openmw/mwscript/globalscripts.hpp @@ -26,16 +26,25 @@ namespace MWWorld namespace MWScript { + struct GlobalScriptDesc + { + bool mRunning; + Locals mLocals; + std::string mId; // ID used to start targeted script (empty if not a targeted script) + + GlobalScriptDesc(); + }; + class GlobalScripts { const MWWorld::ESMStore& mStore; - std::map > mScripts; // running, local variables + std::map mScripts; public: GlobalScripts (const MWWorld::ESMStore& store); - void addScript (const std::string& name); + void addScript (const std::string& name, const std::string& targetId = ""); void removeScript (const std::string& name); @@ -53,7 +62,7 @@ namespace MWScript void write (ESM::ESMWriter& writer, Loading::Listener& progress) const; - bool readRecord (ESM::ESMReader& reader, int32_t type); + bool readRecord (ESM::ESMReader& reader, uint32_t type); ///< Records for variables that do not exist are dropped silently. /// /// \return Known type? diff --git a/apps/openmw/mwscript/guiextensions.cpp b/apps/openmw/mwscript/guiextensions.cpp index afc745beb..40c555f50 100644 --- a/apps/openmw/mwscript/guiextensions.cpp +++ b/apps/openmw/mwscript/guiextensions.cpp @@ -12,7 +12,7 @@ #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" - +#include "../mwbase/world.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "interpretercontext.hpp" @@ -210,6 +210,12 @@ namespace MWScript { bool state = MWBase::Environment::get().getWindowManager()->toggleGui(); runtime.getContext().report(state ? "GUI -> On" : "GUI -> Off"); + + if (!state) + { + while (MWBase::Environment::get().getWindowManager()->getMode() != MWGui::GM_None) // don't use isGuiMode, or we get an infinite loop for modal message boxes! + MWBase::Environment::get().getWindowManager()->popGuiMode(); + } } }; @@ -225,6 +231,8 @@ namespace MWScript new OpShowDialogue (MWGui::GM_Race)); interpreter.installSegment5 (Compiler::Gui::opcodeEnableStatsReviewMenu, new OpShowDialogue (MWGui::GM_Review)); + interpreter.installSegment5 (Compiler::Gui::opcodeEnableLevelupMenu, + new OpShowDialogue (MWGui::GM_Levelup)); interpreter.installSegment5 (Compiler::Gui::opcodeEnableInventoryMenu, new OpEnableWindow (MWGui::GW_Inventory)); diff --git a/apps/openmw/mwscript/interpretercontext.cpp b/apps/openmw/mwscript/interpretercontext.cpp index 028f83a61..c4e0b4261 100644 --- a/apps/openmw/mwscript/interpretercontext.cpp +++ b/apps/openmw/mwscript/interpretercontext.cpp @@ -3,8 +3,14 @@ #include #include +#include #include + +#include + +#include + #include "../mwworld/esmstore.hpp" #include "../mwbase/environment.hpp" @@ -14,6 +20,8 @@ #include "../mwbase/inputmanager.hpp" #include "../mwworld/class.hpp" +#include "../mwworld/cellstore.hpp" +#include "../mwworld/containerstore.hpp" #include "../mwmechanics/npcstats.hpp" @@ -22,7 +30,7 @@ namespace MWScript { - MWWorld::Ptr InterpreterContext::getReference ( + MWWorld::Ptr InterpreterContext::getReferenceImp ( const std::string& id, bool activeOnly, bool doThrow) { if (!id.empty()) @@ -31,6 +39,10 @@ namespace MWScript } else { + if (mReference.isEmpty() && !mTargetId.empty()) + mReference = + MWBase::Environment::get().getWorld()->searchPtr (mTargetId, false); + if (mReference.isEmpty() && doThrow) throw std::runtime_error ("no implicit reference"); @@ -38,7 +50,7 @@ namespace MWScript } } - const MWWorld::Ptr InterpreterContext::getReference ( + const MWWorld::Ptr InterpreterContext::getReferenceImp ( const std::string& id, bool activeOnly, bool doThrow) const { if (!id.empty()) @@ -47,6 +59,10 @@ namespace MWScript } else { + if (mReference.isEmpty() && !mTargetId.empty()) + mReference = + MWBase::Environment::get().getWorld()->searchPtr (mTargetId, false); + if (mReference.isEmpty() && doThrow) throw std::runtime_error ("no implicit reference"); @@ -64,7 +80,7 @@ namespace MWScript } else { - const MWWorld::Ptr ptr = getReference (id, false); + const MWWorld::Ptr ptr = getReferenceImp (id, false); id = ptr.getClass().getScript (ptr); @@ -84,7 +100,7 @@ namespace MWScript } else { - const MWWorld::Ptr ptr = getReference (id, false); + const MWWorld::Ptr ptr = getReferenceImp (id, false); id = ptr.getClass().getScript (ptr); @@ -95,11 +111,43 @@ namespace MWScript } } + int InterpreterContext::findLocalVariableIndex (const std::string& scriptId, + const std::string& name, char type) const + { + int index = MWBase::Environment::get().getScriptManager()->getLocals (scriptId). + searchIndex (type, name); + + if (index!=-1) + return index; + + std::ostringstream stream; + + stream << "Failed to access "; + + switch (type) + { + case 's': stream << "short"; break; + case 'l': stream << "long"; break; + case 'f': stream << "float"; break; + } + + stream << " member variable " << name << " in script " << scriptId; + + throw std::runtime_error (stream.str().c_str()); + } + + InterpreterContext::InterpreterContext ( - MWScript::Locals *locals, MWWorld::Ptr reference) + MWScript::Locals *locals, MWWorld::Ptr reference, const std::string& targetId) : mLocals (locals), mReference (reference), - mActivationHandled (false) - {} + mActivationHandled (false), mTargetId (targetId) + { + // If we run on a reference (local script, dialogue script or console with object + // selected), store the ID of that reference store it so it can be inherited by + // targeted scripts started from this one. + if (targetId.empty() && !reference.isEmpty()) + mTargetId = reference.getClass().getId (reference); + } int InterpreterContext::getLocalShort (int index) const { @@ -152,12 +200,14 @@ namespace MWScript void InterpreterContext::messageBox (const std::string& message, const std::vector& buttons) { - MWBase::Environment::get().getWindowManager()->messageBox (message, buttons); + if (buttons.empty()) + MWBase::Environment::get().getWindowManager()->messageBox (message); + else + MWBase::Environment::get().getWindowManager()->interactiveMessageBox(message, buttons); } void InterpreterContext::report (const std::string& message) { - messageBox (message); } bool InterpreterContext::menuMode() @@ -220,15 +270,21 @@ namespace MWScript std::string InterpreterContext::getActionBinding(const std::string& action) const { - std::vector actions = MWBase::Environment::get().getInputManager()->getActionSorting (); + MWBase::InputManager* input = MWBase::Environment::get().getInputManager(); + std::vector actions = input->getActionKeySorting (); for (std::vector::const_iterator it = actions.begin(); it != actions.end(); ++it) { - std::string desc = MWBase::Environment::get().getInputManager()->getActionDescription (*it); + std::string desc = input->getActionDescription (*it); if(desc == "") continue; if(desc == action) - return MWBase::Environment::get().getInputManager()->getActionBindingName (*it); + { + if(input->joystickLastUsed()) + return input->getActionControllerBindingName(*it); + else + return input->getActionKeyBindingName (*it); + } } return "None"; @@ -236,41 +292,46 @@ namespace MWScript std::string InterpreterContext::getNPCName() const { - ESM::NPC npc = *mReference.get()->mBase; + ESM::NPC npc = *getReferenceImp().get()->mBase; return npc.mName; } std::string InterpreterContext::getNPCRace() const { - ESM::NPC npc = *mReference.get()->mBase; + ESM::NPC npc = *getReferenceImp().get()->mBase; const ESM::Race* race = MWBase::Environment::get().getWorld()->getStore().get().find(npc.mRace); return race->mName; } std::string InterpreterContext::getNPCClass() const { - ESM::NPC npc = *mReference.get()->mBase; + ESM::NPC npc = *getReferenceImp().get()->mBase; const ESM::Class* class_ = MWBase::Environment::get().getWorld()->getStore().get().find(npc.mClass); return class_->mName; } std::string InterpreterContext::getNPCFaction() const { - ESM::NPC npc = *mReference.get()->mBase; + ESM::NPC npc = *getReferenceImp().get()->mBase; const ESM::Faction* faction = MWBase::Environment::get().getWorld()->getStore().get().find(npc.mFaction); return faction->mName; } std::string InterpreterContext::getNPCRank() const { - std::map ranks = mReference.getClass().getNpcStats (mReference).getFactionRanks(); - std::map::const_iterator it = ranks.begin(); + const MWWorld::Ptr& ptr = getReferenceImp(); + std::string faction = ptr.getClass().getPrimaryFaction(ptr); + if (faction.empty()) + throw std::runtime_error("getNPCRank(): NPC is not in a faction"); + + int rank = ptr.getClass().getPrimaryFactionRank(ptr); + if (rank < 0 || rank > 9) + throw std::runtime_error("getNPCRank(): invalid rank"); MWBase::World *world = MWBase::Environment::get().getWorld(); const MWWorld::ESMStore &store = world->getStore(); - const ESM::Faction *faction = store.get().find(it->first); - - return faction->mRanks[it->second]; + const ESM::Faction *fact = store.get().find(faction); + return fact->mRanks[rank]; } std::string InterpreterContext::getPCName() const @@ -299,10 +360,12 @@ namespace MWScript MWBase::World *world = MWBase::Environment::get().getWorld(); MWWorld::Ptr player = world->getPlayerPtr(); - std::string factionId = mReference.getClass().getNpcStats (mReference).getFactionRanks().begin()->first; + std::string factionId = getReferenceImp().getClass().getPrimaryFaction(getReferenceImp()); + if (factionId.empty()) + throw std::runtime_error("getPCRank(): NPC is not in a faction"); - std::map ranks = player.getClass().getNpcStats (player).getFactionRanks(); - std::map::const_iterator it = ranks.find(factionId); + const std::map& ranks = player.getClass().getNpcStats (player).getFactionRanks(); + std::map::const_iterator it = ranks.find(Misc::StringUtils::lowerCase(factionId)); int rank = -1; if (it != ranks.end()) rank = it->second; @@ -326,10 +389,12 @@ namespace MWScript MWBase::World *world = MWBase::Environment::get().getWorld(); MWWorld::Ptr player = world->getPlayerPtr(); - std::string factionId = mReference.getClass().getNpcStats (mReference).getFactionRanks().begin()->first; + std::string factionId = getReferenceImp().getClass().getPrimaryFaction(getReferenceImp()); + if (factionId.empty()) + throw std::runtime_error("getPCNextRank(): NPC is not in a faction"); - std::map ranks = player.getClass().getNpcStats (player).getFactionRanks(); - std::map::const_iterator it = ranks.find(factionId); + const std::map& ranks = player.getClass().getNpcStats (player).getFactionRanks(); + std::map::const_iterator it = ranks.find(Misc::StringUtils::lowerCase(factionId)); int rank = -1; if (it != ranks.end()) rank = it->second; @@ -366,9 +431,9 @@ namespace MWScript return MWBase::Environment::get().getScriptManager()->getGlobalScripts().isRunning (name); } - void InterpreterContext::startScript (const std::string& name) + void InterpreterContext::startScript (const std::string& name, const std::string& targetId) { - MWBase::Environment::get().getScriptManager()->getGlobalScripts().addScript (name); + MWBase::Environment::get().getScriptManager()->getGlobalScripts().addScript (name, targetId); } void InterpreterContext::stopScript (const std::string& name) @@ -383,17 +448,25 @@ namespace MWScript MWWorld::Ptr ref2; if (id.empty()) - ref2 = getReference("", true, true); + ref2 = getReferenceImp(); else - ref2 = MWBase::Environment::get().getWorld()->searchPtr(id, true); + ref2 = MWBase::Environment::get().getWorld()->getPtr(id, false); + + if (ref2.getContainerStore()) // is the object contained? + { + MWWorld::Ptr container = MWBase::Environment::get().getWorld()->findContainer(ref2); - // If either actor is in a non-active cell, return a large value (just like vanilla) - if (ref2.isEmpty()) - return std::numeric_limits().max(); + if (!container.isEmpty()) + ref2 = container; + else + throw std::runtime_error("failed to find container ptr"); + } + + const MWWorld::Ptr ref = MWBase::Environment::get().getWorld()->getPtr(name, false); - const MWWorld::Ptr ref = MWBase::Environment::get().getWorld()->searchPtr(name, true); - if (ref.isEmpty()) - return std::numeric_limits().max(); + // If the objects are in different worldspaces, return a large value (just like vanilla) + if (ref.getCell()->getCell()->getCellId().mWorldspace != ref2.getCell()->getCell()->getCellId().mWorldspace) + return std::numeric_limits::max(); double diff[3]; @@ -435,12 +508,6 @@ namespace MWScript mActivationHandled = true; } - void InterpreterContext::clearActivation() - { - mActivated = MWWorld::Ptr(); - mActivationHandled = false; - } - float InterpreterContext::getSecondsPassed() const { return MWBase::Environment::get().getFrameDuration(); @@ -448,19 +515,19 @@ namespace MWScript bool InterpreterContext::isDisabled (const std::string& id) const { - const MWWorld::Ptr ref = getReference (id, false); + const MWWorld::Ptr ref = getReferenceImp (id, false); return !ref.getRefData().isEnabled(); } void InterpreterContext::enable (const std::string& id) { - MWWorld::Ptr ref = getReference (id, false); + MWWorld::Ptr ref = getReferenceImp (id, false); MWBase::Environment::get().getWorld()->enable (ref); } void InterpreterContext::disable (const std::string& id) { - MWWorld::Ptr ref = getReference (id, false); + MWWorld::Ptr ref = getReferenceImp (id, false); MWBase::Environment::get().getWorld()->disable (ref); } @@ -471,10 +538,7 @@ namespace MWScript const Locals& locals = getMemberLocals (scriptId, global); - int index = MWBase::Environment::get().getScriptManager()->getLocalIndex ( - scriptId, name, 's'); - - return locals.mShorts[index]; + return locals.mShorts[findLocalVariableIndex (scriptId, name, 's')]; } int InterpreterContext::getMemberLong (const std::string& id, const std::string& name, @@ -484,10 +548,7 @@ namespace MWScript const Locals& locals = getMemberLocals (scriptId, global); - int index = MWBase::Environment::get().getScriptManager()->getLocalIndex ( - scriptId, name, 'l'); - - return locals.mLongs[index]; + return locals.mLongs[findLocalVariableIndex (scriptId, name, 'l')]; } float InterpreterContext::getMemberFloat (const std::string& id, const std::string& name, @@ -497,10 +558,7 @@ namespace MWScript const Locals& locals = getMemberLocals (scriptId, global); - int index = MWBase::Environment::get().getScriptManager()->getLocalIndex ( - scriptId, name, 'f'); - - return locals.mFloats[index]; + return locals.mFloats[findLocalVariableIndex (scriptId, name, 'f')]; } void InterpreterContext::setMemberShort (const std::string& id, const std::string& name, @@ -510,10 +568,7 @@ namespace MWScript Locals& locals = getMemberLocals (scriptId, global); - int index = - MWBase::Environment::get().getScriptManager()->getLocalIndex (scriptId, name, 's'); - - locals.mShorts[index] = value; + locals.mShorts[findLocalVariableIndex (scriptId, name, 's')] = value; } void InterpreterContext::setMemberLong (const std::string& id, const std::string& name, int value, bool global) @@ -522,10 +577,7 @@ namespace MWScript Locals& locals = getMemberLocals (scriptId, global); - int index = - MWBase::Environment::get().getScriptManager()->getLocalIndex (scriptId, name, 'l'); - - locals.mLongs[index] = value; + locals.mLongs[findLocalVariableIndex (scriptId, name, 'l')] = value; } void InterpreterContext::setMemberFloat (const std::string& id, const std::string& name, float value, bool global) @@ -534,14 +586,22 @@ namespace MWScript Locals& locals = getMemberLocals (scriptId, global); - int index = - MWBase::Environment::get().getScriptManager()->getLocalIndex (scriptId, name, 'f'); - - locals.mFloats[index] = value; + locals.mFloats[findLocalVariableIndex (scriptId, name, 'f')] = value; } MWWorld::Ptr InterpreterContext::getReference(bool required) { - return getReference ("", true, required); + return getReferenceImp ("", true, required); + } + + std::string InterpreterContext::getTargetId() const + { + return mTargetId; + } + + void InterpreterContext::updatePtr(const MWWorld::Ptr& updated) + { + if (!mReference.isEmpty()) + mReference = updated; } } diff --git a/apps/openmw/mwscript/interpretercontext.hpp b/apps/openmw/mwscript/interpretercontext.hpp index 99026392e..698df62c2 100644 --- a/apps/openmw/mwscript/interpretercontext.hpp +++ b/apps/openmw/mwscript/interpretercontext.hpp @@ -5,8 +5,6 @@ #include -#include "../mwbase/world.hpp" - #include "../mwworld/ptr.hpp" #include "../mwworld/action.hpp" @@ -27,14 +25,22 @@ namespace MWScript class InterpreterContext : public Interpreter::Context { Locals *mLocals; - MWWorld::Ptr mReference; + mutable MWWorld::Ptr mReference; MWWorld::Ptr mActivated; bool mActivationHandled; - MWWorld::Ptr getReference (const std::string& id, bool activeOnly, bool doThrow=true); + std::string mTargetId; + + /// If \a id is empty, a reference the script is run from is returned or in case + /// of a non-local script the reference derived from the target ID. + MWWorld::Ptr getReferenceImp (const std::string& id = "", bool activeOnly = false, + bool doThrow=true); - const MWWorld::Ptr getReference (const std::string& id, bool activeOnly, bool doThrow=true) const; + /// If \a id is empty, a reference the script is run from is returned or in case + /// of a non-local script the reference derived from the target ID. + const MWWorld::Ptr getReferenceImp (const std::string& id = "", + bool activeOnly = false, bool doThrow=true) const; const Locals& getMemberLocals (std::string& id, bool global) const; ///< \a id is changed to the respective script ID, if \a id wasn't a script ID before @@ -42,9 +48,14 @@ namespace MWScript Locals& getMemberLocals (std::string& id, bool global); ///< \a id is changed to the respective script ID, if \a id wasn't a script ID before + /// Throws an exception if local variable can't be found. + int findLocalVariableIndex (const std::string& scriptId, const std::string& name, + char type) const; + public: - InterpreterContext (MWScript::Locals *locals, MWWorld::Ptr reference); + InterpreterContext (MWScript::Locals *locals, MWWorld::Ptr reference, + const std::string& targetId = ""); ///< The ownership of \a locals is not transferred. 0-pointer allowed. virtual int getLocalShort (int index) const; @@ -65,7 +76,7 @@ namespace MWScript const std::vector& buttons); virtual void report (const std::string& message); - ///< By default echo via messageBox. + ///< By default, do nothing. virtual bool menuMode(); @@ -113,7 +124,7 @@ namespace MWScript virtual bool isScriptRunning (const std::string& name) const; - virtual void startScript (const std::string& name); + virtual void startScript (const std::string& name, const std::string& targetId = ""); virtual void stopScript (const std::string& name); @@ -133,9 +144,6 @@ namespace MWScript void executeActivation(MWWorld::Ptr ptr, MWWorld::Ptr actor); ///< Execute the activation action for this ptr. If ptr is mActivated, mark activation as handled. - void clearActivation(); - ///< Discard the action defined by the last activate call. - virtual float getSecondsPassed() const; virtual bool isDisabled (const std::string& id = "") const; @@ -158,6 +166,11 @@ namespace MWScript MWWorld::Ptr getReference(bool required=true); ///< Reference, that the script is running from (can be empty) + + void updatePtr(const MWWorld::Ptr& updated); + ///< Update the Ptr stored in mReference, if there is one stored there. Should be called after the reference has been moved to a new cell. + + virtual std::string getTargetId() const; }; } diff --git a/apps/openmw/mwscript/locals.cpp b/apps/openmw/mwscript/locals.cpp index 57584ac30..9dbf07fba 100644 --- a/apps/openmw/mwscript/locals.cpp +++ b/apps/openmw/mwscript/locals.cpp @@ -10,16 +10,21 @@ #include "../mwbase/environment.hpp" #include "../mwbase/scriptmanager.hpp" +#include + namespace MWScript { void Locals::configure (const ESM::Script& script) { + const Compiler::Locals& locals = + MWBase::Environment::get().getScriptManager()->getLocals (script.mId); + mShorts.clear(); - mShorts.resize (script.mData.mNumShorts, 0); + mShorts.resize (locals.get ('s').size(), 0); mLongs.clear(); - mLongs.resize (script.mData.mNumLongs, 0); + mLongs.resize (locals.get ('l').size(), 0); mFloats.clear(); - mFloats.resize (script.mData.mNumFloats, 0); + mFloats.resize (locals.get ('f').size(), 0); } bool Locals::isEmpty() const @@ -27,9 +32,24 @@ namespace MWScript return (mShorts.empty() && mLongs.empty() && mFloats.empty()); } + bool Locals::hasVar(const std::string &script, const std::string &var) + { + try + { + const Compiler::Locals& locals = + MWBase::Environment::get().getScriptManager()->getLocals(script); + int index = locals.getIndex(var); + return (index != -1); + } + catch (const Compiler::SourceException&) + { + return false; + } + } + int Locals::getIntVar(const std::string &script, const std::string &var) { - Compiler::Locals locals = MWBase::Environment::get().getScriptManager()->getLocals(script); + const Compiler::Locals& locals = MWBase::Environment::get().getScriptManager()->getLocals(script); int index = locals.getIndex(var); char type = locals.getType(var); if(index != -1) @@ -53,7 +73,7 @@ namespace MWScript bool Locals::setVarByInt(const std::string& script, const std::string& var, int val) { - Compiler::Locals locals = MWBase::Environment::get().getScriptManager()->getLocals(script); + const Compiler::Locals& locals = MWBase::Environment::get().getScriptManager()->getLocals(script); int index = locals.getIndex(var); char type = locals.getType(var); if(index != -1) @@ -121,27 +141,60 @@ namespace MWScript const Compiler::Locals& declarations = MWBase::Environment::get().getScriptManager()->getLocals(script); - for (std::vector >::const_iterator iter - = locals.mVariables.begin(); iter!=locals.mVariables.end(); ++iter) + int index = 0, numshorts = 0, numlongs = 0; + for (unsigned int v=0; vfirst); - char index = declarations.getIndex (iter->first); + ESM::VarType type = locals.mVariables[v].second.getType(); + if (type == ESM::VT_Short) + ++numshorts; + else if (type == ESM::VT_Int) + ++numlongs; + } - try + for (std::vector >::const_iterator iter + = locals.mVariables.begin(); iter!=locals.mVariables.end(); ++iter,++index) + { + if (iter->first.empty()) { - switch (type) + // no variable names available (this will happen for legacy, i.e. ESS-imported savegames only) + try { - case 's': mShorts.at (index) = iter->second.getInteger(); break; - case 'l': mLongs.at (index) = iter->second.getInteger(); break; - case 'f': mFloats.at (index) = iter->second.getFloat(); break; - - // silently ignore locals that don't exist anymore + if (index >= numshorts+numlongs) + mFloats.at(index - (numshorts+numlongs)) = iter->second.getFloat(); + else if (index >= numshorts) + mLongs.at(index - numshorts) = iter->second.getInteger(); + else + mShorts.at(index) = iter->second.getInteger(); + } + catch (std::exception& e) + { + std::cerr << "Failed to read local variable state for script '" + << script << "' (legacy format): " << e.what() + << "\nNum shorts: " << numshorts << " / " << mShorts.size() + << " Num longs: " << numlongs << " / " << mLongs.size() << std::endl; } } - catch (...) + else { - // ignore type changes - /// \todo write to log + char type = declarations.getType (iter->first); + char index = declarations.getIndex (iter->first); + + try + { + switch (type) + { + case 's': mShorts.at (index) = iter->second.getInteger(); break; + case 'l': mLongs.at (index) = iter->second.getInteger(); break; + case 'f': mFloats.at (index) = iter->second.getFloat(); break; + + // silently ignore locals that don't exist anymore + } + } + catch (...) + { + // ignore type changes + /// \todo write to log + } } } } diff --git a/apps/openmw/mwscript/locals.hpp b/apps/openmw/mwscript/locals.hpp index 1e8c6e12a..bd95835ac 100644 --- a/apps/openmw/mwscript/locals.hpp +++ b/apps/openmw/mwscript/locals.hpp @@ -24,8 +24,15 @@ namespace MWScript bool isEmpty() const; void configure (const ESM::Script& script); + + /// @note var needs to be in lowercase bool setVarByInt(const std::string& script, const std::string& var, int val); - int getIntVar (const std::string& script, const std::string& var); ///< if var does not exist, returns 0 + + bool hasVar(const std::string& script, const std::string& var); + + /// if var does not exist, returns 0 + /// @note var needs to be in lowercase + int getIntVar (const std::string& script, const std::string& var); void write (ESM::Locals& locals, const std::string& script) const; diff --git a/apps/openmw/mwscript/miscextensions.cpp b/apps/openmw/mwscript/miscextensions.cpp index 4e0257d82..52094947c 100644 --- a/apps/openmw/mwscript/miscextensions.cpp +++ b/apps/openmw/mwscript/miscextensions.cpp @@ -3,8 +3,6 @@ #include -#include - #include #include #include @@ -19,6 +17,7 @@ #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/scriptmanager.hpp" +#include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "../mwworld/player.hpp" @@ -33,6 +32,42 @@ #include "interpretercontext.hpp" #include "ref.hpp" +namespace +{ + + void addToLevList(ESM::LevelledListBase* list, const std::string& itemId, int level) + { + for (std::vector::iterator it = list->mList.begin(); it != list->mList.end();) + { + if (it->mLevel == level && itemId == it->mId) + return; + } + + ESM::LevelledListBase::LevelItem item; + item.mId = itemId; + item.mLevel = level; + list->mList.push_back(item); + } + + void removeFromLevList(ESM::LevelledListBase* list, const std::string& itemId, int level) + { + // level of -1 removes all items with that itemId + for (std::vector::iterator it = list->mList.begin(); it != list->mList.end();) + { + if (level != -1 && it->mLevel != level) + { + ++it; + continue; + } + if (Misc::StringUtils::ciEqual(itemId, it->mId)) + it = list->mList.erase(it); + else + ++it; + } + } + +} + namespace MWScript { namespace Misc @@ -150,7 +185,7 @@ namespace MWScript // Instantly reset door to closed state // This is done when using Lock in scripts, but not when using Lock spells. - if (ptr.getTypeName() == typeid(ESM::Door).name()) + if (ptr.getTypeName() == typeid(ESM::Door).name() && !ptr.getCellRef().getTeleport()) { MWBase::Environment::get().getWorld()->activateDoor(ptr, 0); MWBase::Environment::get().getWorld()->localRotateObject(ptr, 0, 0, 0); @@ -248,7 +283,7 @@ namespace MWScript Interpreter::Type_Float time = runtime[0].mFloat; runtime.pop(); - MWBase::Environment::get().getWorld()->getFader()->fadeIn(time); + MWBase::Environment::get().getWindowManager()->fadeScreenIn(time, false); } }; @@ -261,7 +296,7 @@ namespace MWScript Interpreter::Type_Float time = runtime[0].mFloat; runtime.pop(); - MWBase::Environment::get().getWorld()->getFader()->fadeOut(time); + MWBase::Environment::get().getWindowManager()->fadeScreenOut(time, false); } }; @@ -277,7 +312,7 @@ namespace MWScript Interpreter::Type_Float time = runtime[0].mFloat; runtime.pop(); - MWBase::Environment::get().getWorld()->getFader()->fadeTo(alpha, time); + MWBase::Environment::get().getWindowManager()->fadeScreenTo(alpha, time, false); } }; @@ -292,6 +327,17 @@ namespace MWScript } }; + class OpToggleWorld : public Interpreter::Opcode0 + { + public: + + virtual void execute (Interpreter::Runtime& runtime) + { + runtime.getContext().report(MWBase::Environment::get().getWorld()->toggleWorld() ? "World -> On" + : "World -> Off"); + } + }; + class OpDontSaveObject : public Interpreter::Opcode0 { public: @@ -303,6 +349,35 @@ namespace MWScript } }; + class OpPcForce1stPerson : public Interpreter::Opcode0 + { + public: + + virtual void execute (Interpreter::Runtime& runtime) + { + if (!MWBase::Environment::get().getWorld()->isFirstPerson()) + MWBase::Environment::get().getWorld()->togglePOV(); + } + }; + + class OpPcForce3rdPerson : public Interpreter::Opcode0 + { + virtual void execute (Interpreter::Runtime& runtime) + { + if (MWBase::Environment::get().getWorld()->isFirstPerson()) + MWBase::Environment::get().getWorld()->togglePOV(); + } + }; + + class OpPcGet3rdPerson : public Interpreter::Opcode0 + { + public: + virtual void execute(Interpreter::Runtime& runtime) + { + runtime.push(!MWBase::Environment::get().getWorld()->isFirstPerson()); + } + }; + class OpToggleVanityMode : public Interpreter::Opcode0 { static bool sActivate; @@ -358,7 +433,7 @@ namespace MWScript key = ESM::MagicEffect::effectStringToId(effect); runtime.push(ptr.getClass().getCreatureStats(ptr).getMagicEffects().get( - MWMechanics::EffectKey(key)).mMagnitude > 0); + MWMechanics::EffectKey(key)).getMagnitude() > 0); } }; @@ -445,7 +520,8 @@ namespace MWScript if (::Misc::StringUtils::ciEqual(iter->getCellRef().getRefId(), item)) { int removed = store.remove(*iter, toRemove, ptr); - MWBase::Environment::get().getWorld()->dropObjectOnGround(ptr, *iter, removed); + MWWorld::Ptr dropped = MWBase::Environment::get().getWorld()->dropObjectOnGround(ptr, *iter, removed); + dropped.getCellRef().setOwner(""); toRemove -= removed; @@ -561,6 +637,10 @@ namespace MWScript if (parameter == 1) MWBase::Environment::get().getWorld()->deleteObject(ptr); + else if (parameter == 0) + MWBase::Environment::get().getWorld()->undeleteObject(ptr); + else + throw std::runtime_error("SetDelete: unexpected parameter"); } }; @@ -611,6 +691,60 @@ namespace MWScript } }; + template + class OpGetCollidingPc : public Interpreter::Opcode0 + { + public: + + virtual void execute (Interpreter::Runtime& runtime) + { + MWWorld::Ptr ptr = R()(runtime); + runtime.push (MWBase::Environment::get().getWorld()->getPlayerCollidingWith(ptr)); + } + }; + + template + class OpGetCollidingActor : public Interpreter::Opcode0 + { + public: + + virtual void execute (Interpreter::Runtime& runtime) + { + MWWorld::Ptr ptr = R()(runtime); + runtime.push (MWBase::Environment::get().getWorld()->getActorCollidingWith(ptr)); + } + }; + + template + class OpHurtStandingActor : public Interpreter::Opcode0 + { + public: + + virtual void execute (Interpreter::Runtime& runtime) + { + MWWorld::Ptr ptr = R()(runtime); + float healthDiffPerSecond = runtime[0].mFloat; + runtime.pop(); + + MWBase::Environment::get().getWorld()->hurtStandingActors(ptr, healthDiffPerSecond); + } + }; + + template + class OpHurtCollidingActor : public Interpreter::Opcode0 + { + public: + + virtual void execute (Interpreter::Runtime& runtime) + { + MWWorld::Ptr ptr = R()(runtime); + float healthDiffPerSecond = runtime[0].mFloat; + runtime.pop(); + + MWBase::Environment::get().getWorld()->hurtCollidingActors(ptr, healthDiffPerSecond); + } + }; + class OpGetWindSpeed : public Interpreter::Opcode0 { public: @@ -635,6 +769,27 @@ namespace MWScript MWMechanics::CreatureStats &stats = ptr.getClass().getCreatureStats(ptr); runtime.push(::Misc::StringUtils::ciEqual(objectID, stats.getLastHitObject())); + + stats.setLastHitObject(std::string()); + } + }; + + template + class OpHitAttemptOnMe : public Interpreter::Opcode0 + { + public: + + virtual void execute (Interpreter::Runtime& runtime) + { + MWWorld::Ptr ptr = R()(runtime); + + std::string objectID = runtime.getStringLiteral (runtime[0].mInteger); + runtime.pop(); + + MWMechanics::CreatureStats &stats = ptr.getClass().getCreatureStats(ptr); + runtime.push(::Misc::StringUtils::ciEqual(objectID, stats.getLastHitAttemptObject())); + + stats.setLastHitAttemptObject(std::string()); } }; @@ -760,6 +915,19 @@ namespace MWScript } }; + class OpToggleScripts : public Interpreter::Opcode0 + { + public: + virtual void execute (Interpreter::Runtime& runtime) + { + InterpreterContext& context = static_cast (runtime.getContext()); + + bool enabled = MWBase::Environment::get().getWorld()->toggleScripts(); + + context.report(enabled ? "Scripts -> On" : "Scripts -> Off"); + } + }; + class OpToggleGodMode : public Interpreter::Opcode0 { public: @@ -791,6 +959,7 @@ namespace MWScript MWMechanics::CastSpell cast(ptr, target); cast.mHitPosition = Ogre::Vector3(target.getRefData().getPosition().pos); + cast.mAlwaysSucceed = true; cast.cast(spell); } }; @@ -808,6 +977,7 @@ namespace MWScript MWMechanics::CastSpell cast(ptr, ptr); cast.mHitPosition = Ogre::Vector3(ptr.getRefData().getPosition().pos); + cast.mAlwaysSucceed = true; cast.cast(spell); } }; @@ -819,7 +989,6 @@ namespace MWScript { MWBase::World* world = MWBase::Environment::get().getWorld(); world->goToJail(); - MWBase::Environment::get().getWorld()->getPlayer().recordCrimeId(); } }; @@ -852,8 +1021,7 @@ namespace MWScript virtual void execute (Interpreter::Runtime &runtime) { - /// \todo implement jail check - runtime.push (0); + runtime.push (MWBase::Environment::get().getWindowManager()->containsMode(MWGui::GM_Jail)); } }; @@ -880,13 +1048,14 @@ namespace MWScript msg << "Content file: "; - if (ptr.getCellRef().getRefNum().mContentFile == -1) + if (!ptr.getCellRef().hasContentFile()) msg << "[None]" << std::endl; else { std::vector contentFiles = MWBase::Environment::get().getWorld()->getContentFiles(); msg << contentFiles.at (ptr.getCellRef().getRefNum().mContentFile) << std::endl; + msg << "RefNum: " << ptr.getCellRef().getRefNum().mIndex << std::endl; } msg << "RefID: " << ptr.getCellRef().getRefId() << std::endl; @@ -899,6 +1068,9 @@ namespace MWScript msg << "Grid: " << cell->getCell()->getGridX() << " " << cell->getCell()->getGridY() << std::endl; Ogre::Vector3 pos (ptr.getRefData().getPosition().pos); msg << "Coordinates: " << pos << std::endl; + msg << "Model: " << ptr.getClass().getModel(ptr) << std::endl; + if (!ptr.getClass().getScript(ptr).empty()) + msg << "Script: " << ptr.getClass().getScript(ptr) << std::endl; } std::string notes = runtime.getStringLiteral (runtime[0].mInteger); @@ -910,6 +1082,78 @@ namespace MWScript } }; + class OpAddToLevCreature : public Interpreter::Opcode0 + { + public: + virtual void execute(Interpreter::Runtime &runtime) + { + const std::string& levId = runtime.getStringLiteral(runtime[0].mInteger); + runtime.pop(); + const std::string& creatureId = runtime.getStringLiteral(runtime[0].mInteger); + runtime.pop(); + int level = runtime[0].mInteger; + runtime.pop(); + + ESM::CreatureLevList listCopy = *MWBase::Environment::get().getWorld()->getStore().get().find(levId); + addToLevList(&listCopy, creatureId, level); + MWBase::Environment::get().getWorld()->createOverrideRecord(listCopy); + } + }; + + class OpRemoveFromLevCreature : public Interpreter::Opcode0 + { + public: + virtual void execute(Interpreter::Runtime &runtime) + { + const std::string& levId = runtime.getStringLiteral(runtime[0].mInteger); + runtime.pop(); + const std::string& creatureId = runtime.getStringLiteral(runtime[0].mInteger); + runtime.pop(); + int level = runtime[0].mInteger; + runtime.pop(); + + ESM::CreatureLevList listCopy = *MWBase::Environment::get().getWorld()->getStore().get().find(levId); + removeFromLevList(&listCopy, creatureId, level); + MWBase::Environment::get().getWorld()->createOverrideRecord(listCopy); + } + }; + + class OpAddToLevItem : public Interpreter::Opcode0 + { + public: + virtual void execute(Interpreter::Runtime &runtime) + { + const std::string& levId = runtime.getStringLiteral(runtime[0].mInteger); + runtime.pop(); + const std::string& itemId = runtime.getStringLiteral(runtime[0].mInteger); + runtime.pop(); + int level = runtime[0].mInteger; + runtime.pop(); + + ESM::ItemLevList listCopy = *MWBase::Environment::get().getWorld()->getStore().get().find(levId); + addToLevList(&listCopy, itemId, level); + MWBase::Environment::get().getWorld()->createOverrideRecord(listCopy); + } + }; + + class OpRemoveFromLevItem : public Interpreter::Opcode0 + { + public: + virtual void execute(Interpreter::Runtime &runtime) + { + const std::string& levId = runtime.getStringLiteral(runtime[0].mInteger); + runtime.pop(); + const std::string& itemId = runtime.getStringLiteral(runtime[0].mInteger); + runtime.pop(); + int level = runtime[0].mInteger; + runtime.pop(); + + ESM::ItemLevList listCopy = *MWBase::Environment::get().getWorld()->getStore().get().find(levId); + removeFromLevList(&listCopy, itemId, level); + MWBase::Environment::get().getWorld()->createOverrideRecord(listCopy); + } + }; + void installOpcodes (Interpreter::Interpreter& interpreter) { interpreter.installSegment5 (Compiler::Misc::opcodeXBox, new OpXBox); @@ -928,7 +1172,11 @@ namespace MWScript interpreter.installSegment5 (Compiler::Misc::opcodeFadeTo, new OpFadeTo); interpreter.installSegment5 (Compiler::Misc::opcodeTogglePathgrid, new OpTogglePathgrid); interpreter.installSegment5 (Compiler::Misc::opcodeToggleWater, new OpToggleWater); + interpreter.installSegment5 (Compiler::Misc::opcodeToggleWorld, new OpToggleWorld); interpreter.installSegment5 (Compiler::Misc::opcodeDontSaveObject, new OpDontSaveObject); + interpreter.installSegment5 (Compiler::Misc::opcodePcForce1stPerson, new OpPcForce1stPerson); + interpreter.installSegment5 (Compiler::Misc::opcodePcForce3rdPerson, new OpPcForce3rdPerson); + interpreter.installSegment5 (Compiler::Misc::opcodePcGet3rdPerson, new OpPcGet3rdPerson); interpreter.installSegment5 (Compiler::Misc::opcodeToggleVanityMode, new OpToggleVanityMode); interpreter.installSegment5 (Compiler::Misc::opcodeGetPcSleep, new OpGetPcSleep); interpreter.installSegment5 (Compiler::Misc::opcodeGetPcJumping, new OpGetPcJumping); @@ -967,14 +1215,25 @@ namespace MWScript interpreter.installSegment5 (Compiler::Misc::opcodeGetStandingPcExplicit, new OpGetStandingPc); interpreter.installSegment5 (Compiler::Misc::opcodeGetStandingActor, new OpGetStandingActor); interpreter.installSegment5 (Compiler::Misc::opcodeGetStandingActorExplicit, new OpGetStandingActor); + interpreter.installSegment5 (Compiler::Misc::opcodeGetCollidingPc, new OpGetCollidingPc); + interpreter.installSegment5 (Compiler::Misc::opcodeGetCollidingPcExplicit, new OpGetCollidingPc); + interpreter.installSegment5 (Compiler::Misc::opcodeGetCollidingActor, new OpGetCollidingActor); + interpreter.installSegment5 (Compiler::Misc::opcodeGetCollidingActorExplicit, new OpGetCollidingActor); + interpreter.installSegment5 (Compiler::Misc::opcodeHurtStandingActor, new OpHurtStandingActor); + interpreter.installSegment5 (Compiler::Misc::opcodeHurtStandingActorExplicit, new OpHurtStandingActor); + interpreter.installSegment5 (Compiler::Misc::opcodeHurtCollidingActor, new OpHurtCollidingActor); + interpreter.installSegment5 (Compiler::Misc::opcodeHurtCollidingActorExplicit, new OpHurtCollidingActor); interpreter.installSegment5 (Compiler::Misc::opcodeGetWindSpeed, new OpGetWindSpeed); interpreter.installSegment5 (Compiler::Misc::opcodeHitOnMe, new OpHitOnMe); interpreter.installSegment5 (Compiler::Misc::opcodeHitOnMeExplicit, new OpHitOnMe); + interpreter.installSegment5 (Compiler::Misc::opcodeHitAttemptOnMe, new OpHitAttemptOnMe); + interpreter.installSegment5 (Compiler::Misc::opcodeHitAttemptOnMeExplicit, new OpHitAttemptOnMe); interpreter.installSegment5 (Compiler::Misc::opcodeDisableTeleporting, new OpEnableTeleporting); interpreter.installSegment5 (Compiler::Misc::opcodeEnableTeleporting, new OpEnableTeleporting); interpreter.installSegment5 (Compiler::Misc::opcodeShowVars, new OpShowVars); interpreter.installSegment5 (Compiler::Misc::opcodeShowVarsExplicit, new OpShowVars); interpreter.installSegment5 (Compiler::Misc::opcodeToggleGodMode, new OpToggleGodMode); + interpreter.installSegment5 (Compiler::Misc::opcodeToggleScripts, new OpToggleScripts); interpreter.installSegment5 (Compiler::Misc::opcodeDisableLevitation, new OpEnableLevitation); interpreter.installSegment5 (Compiler::Misc::opcodeEnableLevitation, new OpEnableLevitation); interpreter.installSegment5 (Compiler::Misc::opcodeCast, new OpCast); @@ -985,6 +1244,10 @@ namespace MWScript interpreter.installSegment5 (Compiler::Misc::opcodeGetPcTraveling, new OpGetPcTraveling); interpreter.installSegment5 (Compiler::Misc::opcodeBetaComment, new OpBetaComment); interpreter.installSegment5 (Compiler::Misc::opcodeBetaCommentExplicit, new OpBetaComment); + interpreter.installSegment5 (Compiler::Misc::opcodeAddToLevCreature, new OpAddToLevCreature); + interpreter.installSegment5 (Compiler::Misc::opcodeRemoveFromLevCreature, new OpRemoveFromLevCreature); + interpreter.installSegment5 (Compiler::Misc::opcodeAddToLevItem, new OpAddToLevItem); + interpreter.installSegment5 (Compiler::Misc::opcodeRemoveFromLevItem, new OpRemoveFromLevItem); } } } diff --git a/apps/openmw/mwscript/ref.cpp b/apps/openmw/mwscript/ref.cpp new file mode 100644 index 000000000..6347c2c2e --- /dev/null +++ b/apps/openmw/mwscript/ref.cpp @@ -0,0 +1,29 @@ +#include "ref.hpp" + +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" + +#include "interpretercontext.hpp" + +MWWorld::Ptr MWScript::ExplicitRef::operator() (Interpreter::Runtime& runtime, bool required, + bool activeOnly) const +{ + std::string id = runtime.getStringLiteral(runtime[0].mInteger); + runtime.pop(); + + if (required) + return MWBase::Environment::get().getWorld()->getPtr(id, activeOnly); + else + return MWBase::Environment::get().getWorld()->searchPtr(id, activeOnly); +} + +MWWorld::Ptr MWScript::ImplicitRef::operator() (Interpreter::Runtime& runtime, bool required, + bool activeOnly) const +{ + MWScript::InterpreterContext& context + = static_cast (runtime.getContext()); + + return context.getReference(required); +} diff --git a/apps/openmw/mwscript/ref.hpp b/apps/openmw/mwscript/ref.hpp index 795839139..e572f5147 100644 --- a/apps/openmw/mwscript/ref.hpp +++ b/apps/openmw/mwscript/ref.hpp @@ -3,37 +3,29 @@ #include -#include - -#include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" - #include "../mwworld/ptr.hpp" -#include "interpretercontext.hpp" +namespace Interpreter +{ + class Runtime; +} namespace MWScript { struct ExplicitRef { - MWWorld::Ptr operator() (Interpreter::Runtime& runtime, bool required=true) const - { - std::string id = runtime.getStringLiteral (runtime[0].mInteger); - runtime.pop(); + static const bool implicit = false; - return MWBase::Environment::get().getWorld()->getPtr (id, false); - } + MWWorld::Ptr operator() (Interpreter::Runtime& runtime, bool required = true, + bool activeOnly = false) const; }; struct ImplicitRef { - MWWorld::Ptr operator() (Interpreter::Runtime& runtime, bool required=true) const - { - MWScript::InterpreterContext& context - = static_cast (runtime.getContext()); + static const bool implicit = true; - return context.getReference(required); - } + MWWorld::Ptr operator() (Interpreter::Runtime& runtime, bool required = true, + bool activeOnly = false) const; }; } diff --git a/apps/openmw/mwscript/scriptmanagerimp.cpp b/apps/openmw/mwscript/scriptmanagerimp.cpp index 7b858dacf..5f755e542 100644 --- a/apps/openmw/mwscript/scriptmanagerimp.cpp +++ b/apps/openmw/mwscript/scriptmanagerimp.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include @@ -22,12 +23,19 @@ namespace MWScript { ScriptManager::ScriptManager (const MWWorld::ESMStore& store, bool verbose, - Compiler::Context& compilerContext, int warningsMode) + Compiler::Context& compilerContext, int warningsMode, + const std::vector& scriptBlacklist) : mErrorHandler (std::cerr), mStore (store), mVerbose (verbose), mCompilerContext (compilerContext), mParser (mErrorHandler, mCompilerContext), mOpcodesInstalled (false), mGlobalScripts (store) { mErrorHandler.setWarningsMode (warningsMode); + + mScriptBlacklist.resize (scriptBlacklist.size()); + + std::transform (scriptBlacklist.begin(), scriptBlacklist.end(), + mScriptBlacklist.begin(), Misc::StringUtils::lowerCase); + std::sort (mScriptBlacklist.begin(), mScriptBlacklist.end()); } bool ScriptManager::compile (const std::string& name) @@ -35,13 +43,12 @@ namespace MWScript mParser.reset(); mErrorHandler.reset(); - bool Success = true; - if (const ESM::Script *script = mStore.get().find (name)) { if (mVerbose) std::cout << "compiling script: " << name << std::endl; + bool Success = true; try { std::istringstream input (script->mScriptText); @@ -78,8 +85,6 @@ namespace MWScript mParser.getCode (code); mScripts.insert (std::make_pair (name, std::make_pair (code, mParser.getLocals()))); - // TODO sanity check on generated locals - return true; } } @@ -133,16 +138,22 @@ namespace MWScript int success = 0; const MWWorld::Store& scripts = mStore.get(); - MWWorld::Store::iterator it = scripts.begin(); - for (; it != scripts.end(); ++it, ++count) - if (compile (it->mId)) - ++success; + for (MWWorld::Store::iterator iter = scripts.begin(); + iter != scripts.end(); ++iter) + if (!std::binary_search (mScriptBlacklist.begin(), mScriptBlacklist.end(), + Misc::StringUtils::lowerCase (iter->mId))) + { + ++count; + + if (compile (iter->mId)) + ++success; + } return std::make_pair (count, success); } - Compiler::Locals& ScriptManager::getLocals (const std::string& name) + const Compiler::Locals& ScriptManager::getLocals (const std::string& name) { std::string name2 = Misc::StringUtils::lowerCase (name); @@ -162,6 +173,11 @@ namespace MWScript if (const ESM::Script *script = mStore.get().find (name2)) { + if (mVerbose) + std::cout + << "scanning script for local variable declarations: " << name2 + << std::endl; + Compiler::Locals locals; std::istringstream stream (script->mScriptText); @@ -182,46 +198,4 @@ namespace MWScript { return mGlobalScripts; } - - int ScriptManager::getLocalIndex (const std::string& scriptId, const std::string& variable, - char type) - { - const ESM::Script *script = mStore.get().find (scriptId); - - int offset = 0; - int size = 0; - - switch (type) - { - case 's': - - offset = 0; - size = script->mData.mNumShorts; - break; - - case 'l': - - offset = script->mData.mNumShorts; - size = script->mData.mNumLongs; - break; - - case 'f': - - offset = script->mData.mNumShorts+script->mData.mNumLongs; - size = script->mData.mNumFloats; - break; - - default: - - throw std::runtime_error ("invalid variable type"); - } - - std::string variable2 = Misc::StringUtils::lowerCase (variable); - - for (int i=0; imVarNames.at (i+offset))==variable2) - return i; - - throw std::runtime_error ("unable to access local variable " + variable + " of " + scriptId); - } } diff --git a/apps/openmw/mwscript/scriptmanagerimp.hpp b/apps/openmw/mwscript/scriptmanagerimp.hpp index da3abc60b..6026f6aba 100644 --- a/apps/openmw/mwscript/scriptmanagerimp.hpp +++ b/apps/openmw/mwscript/scriptmanagerimp.hpp @@ -48,11 +48,13 @@ namespace MWScript ScriptCollection mScripts; GlobalScripts mGlobalScripts; std::map mOtherLocals; + std::vector mScriptBlacklist; public: ScriptManager (const MWWorld::ESMStore& store, bool verbose, - Compiler::Context& compilerContext, int warningsMode); + Compiler::Context& compilerContext, int warningsMode, + const std::vector& scriptBlacklist); virtual void run (const std::string& name, Interpreter::Context& interpreterContext); ///< Run the script with the given name (compile first, if not compiled yet) @@ -65,15 +67,10 @@ namespace MWScript ///< Compile all scripts /// \return count, success - virtual Compiler::Locals& getLocals (const std::string& name); + virtual const Compiler::Locals& getLocals (const std::string& name); ///< Return locals for script \a name. virtual GlobalScripts& getGlobalScripts(); - - virtual int getLocalIndex (const std::string& scriptId, const std::string& variable, - char type); - ///< Return index of the variable of the given name and type in the given script. Will - /// throw an exception, if there is no such script or variable or the type does not match. }; } diff --git a/apps/openmw/mwscript/skyextensions.cpp b/apps/openmw/mwscript/skyextensions.cpp index 8b9efd74e..0ccd0ce31 100644 --- a/apps/openmw/mwscript/skyextensions.cpp +++ b/apps/openmw/mwscript/skyextensions.cpp @@ -9,6 +9,7 @@ #include #include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" #include "interpretercontext.hpp" diff --git a/apps/openmw/mwscript/soundextensions.cpp b/apps/openmw/mwscript/soundextensions.cpp index 73c3ec93a..606de7aa0 100644 --- a/apps/openmw/mwscript/soundextensions.cpp +++ b/apps/openmw/mwscript/soundextensions.cpp @@ -121,8 +121,8 @@ namespace MWScript MWBase::Environment::get().getSoundManager()->playSound3D(ptr, sound, 1.0, 1.0, MWBase::SoundManager::Play_TypeSfx, - mLoop ? MWBase::SoundManager::Play_Loop : - MWBase::SoundManager::Play_Normal); + mLoop ? MWBase::SoundManager::Play_LoopRemoveAtDistance + : MWBase::SoundManager::Play_Normal); } }; @@ -150,8 +150,8 @@ namespace MWScript MWBase::Environment::get().getSoundManager()->playSound3D(ptr, sound, volume, pitch, MWBase::SoundManager::Play_TypeSfx, - mLoop ? MWBase::SoundManager::Play_Loop : - MWBase::SoundManager::Play_Normal); + mLoop ? MWBase::SoundManager::Play_LoopRemoveAtDistance + : MWBase::SoundManager::Play_Normal); } }; diff --git a/apps/openmw/mwscript/statsextensions.cpp b/apps/openmw/mwscript/statsextensions.cpp index 07d137ce2..b8bb76388 100644 --- a/apps/openmw/mwscript/statsextensions.cpp +++ b/apps/openmw/mwscript/statsextensions.cpp @@ -18,8 +18,10 @@ #include "../mwbase/dialoguemanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" #include "../mwworld/class.hpp" +#include "../mwworld/player.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/npcstats.hpp" @@ -31,13 +33,12 @@ namespace { std::string getDialogueActorFaction(MWWorld::Ptr actor) { - const MWMechanics::NpcStats &stats = actor.getClass().getNpcStats (actor); - - if (stats.getFactionRanks().empty()) + std::string factionId = actor.getClass().getPrimaryFaction(actor); + if (factionId.empty()) throw std::runtime_error ( "failed to determine dialogue actors faction (because actor is factionless)"); - return stats.getFactionRanks().begin()->first; + return factionId; } } @@ -217,11 +218,28 @@ namespace MWScript virtual void execute (Interpreter::Runtime& runtime) { + int peek = R::implicit ? 0 : runtime[0].mInteger; + MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Float diff = runtime[0].mFloat; runtime.pop(); + // workaround broken endgame scripts that kill dagoth ur + if (!R::implicit && + Misc::StringUtils::ciEqual(ptr.getCellRef().getRefId(), "dagoth_ur_1")) + { + runtime.push (peek); + + if (R()(runtime, false, true).isEmpty()) + { + std::cerr + << "Compensating for broken script in Morrowind.esm by " + << "ignoring remote access to dagoth_ur_1" << std::endl; + return; + } + } + MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats (ptr); Interpreter::Type_Float current = stats.getDynamic(mIndex).getCurrent(); @@ -260,7 +278,9 @@ namespace MWScript MWMechanics::DynamicStat stat (ptr.getClass().getCreatureStats (ptr) .getDynamic (mIndex)); - stat.setCurrent (diff + current, true); + // for fatigue, a negative current value is allowed and means the actor will be knocked down + bool allowDecreaseBelowZero = (mIndex == 2); + stat.setCurrent (diff + current, allowDecreaseBelowZero); ptr.getClass().getCreatureStats (ptr).setDynamic (mIndex, stat); } @@ -329,29 +349,12 @@ namespace MWScript MWMechanics::NpcStats& stats = ptr.getClass().getNpcStats (ptr); - MWWorld::LiveCellRef *ref = ptr.get(); - - assert (ref); - - const ESM::Class& class_ = - *MWBase::Environment::get().getWorld()->getStore().get().find (ref->mBase->mClass); - - float level = stats.getSkill(mIndex).getBase(); - float progress = stats.getSkill(mIndex).getProgress(); - int newLevel = value - (stats.getSkill(mIndex).getModified() - stats.getSkill(mIndex).getBase()); if (newLevel<0) newLevel = 0; - progress = (progress / stats.getSkillGain (mIndex, class_, -1, level)) - * stats.getSkillGain (mIndex, class_, -1, newLevel); - - if (progress>=1) - progress = 0.999999999; - stats.getSkill (mIndex).setBase (newLevel); - stats.getSkill (mIndex).setProgress(progress); } }; @@ -399,8 +402,12 @@ namespace MWScript MWBase::World *world = MWBase::Environment::get().getWorld(); MWWorld::Ptr player = world->getPlayerPtr(); - player.getClass().getNpcStats (player).setBounty(runtime[0].mFloat); + int bounty = runtime[0].mFloat; runtime.pop(); + player.getClass().getNpcStats (player).setBounty(bounty); + + if (bounty == 0) + MWBase::Environment::get().getWorld()->getPlayer().recordCrimeId(); } }; @@ -528,11 +535,12 @@ namespace MWScript virtual void execute (Interpreter::Runtime& runtime, unsigned int arg0) { + MWWorld::Ptr actor = R()(runtime, false); + std::string factionID = ""; if(arg0==0) { - MWWorld::Ptr actor = R()(runtime); factionID = getDialogueActorFaction(actor); } else @@ -547,10 +555,7 @@ namespace MWScript if(factionID != "") { MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); - if(player.getClass().getNpcStats(player).getFactionRanks().find(factionID) == player.getClass().getNpcStats(player).getFactionRanks().end()) - { - player.getClass().getNpcStats(player).getFactionRanks()[factionID] = 0; - } + player.getClass().getNpcStats(player).joinFaction(factionID); } } }; @@ -562,11 +567,12 @@ namespace MWScript virtual void execute (Interpreter::Runtime& runtime, unsigned int arg0) { + MWWorld::Ptr actor = R()(runtime, false); + std::string factionID = ""; if(arg0==0) { - MWWorld::Ptr actor = R()(runtime); factionID = getDialogueActorFaction(actor); } else @@ -583,13 +589,11 @@ namespace MWScript MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); if(player.getClass().getNpcStats(player).getFactionRanks().find(factionID) == player.getClass().getNpcStats(player).getFactionRanks().end()) { - player.getClass().getNpcStats(player).getFactionRanks()[factionID] = 0; + player.getClass().getNpcStats(player).joinFaction(factionID); } else { - player.getClass().getNpcStats(player).getFactionRanks()[factionID] = - std::min(player.getClass().getNpcStats(player).getFactionRanks()[factionID] +1, - 9); + player.getClass().getNpcStats(player).raiseRank(factionID); } } } @@ -602,11 +606,12 @@ namespace MWScript virtual void execute (Interpreter::Runtime& runtime, unsigned int arg0) { + MWWorld::Ptr actor = R()(runtime, false); + std::string factionID = ""; if(arg0==0) { - MWWorld::Ptr actor = R()(runtime); factionID = getDialogueActorFaction(actor); } else @@ -621,11 +626,7 @@ namespace MWScript if(factionID != "") { MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); - if(player.getClass().getNpcStats(player).getFactionRanks().find(factionID) != player.getClass().getNpcStats(player).getFactionRanks().end()) - { - player.getClass().getNpcStats(player).getFactionRanks()[factionID] = - std::max(0, player.getClass().getNpcStats(player).getFactionRanks()[factionID]-1); - } + player.getClass().getNpcStats(player).lowerRank(factionID); } } }; @@ -637,7 +638,7 @@ namespace MWScript virtual void execute (Interpreter::Runtime& runtime, unsigned int arg0) { - MWWorld::Ptr ptr = R()(runtime); + MWWorld::Ptr ptr = R()(runtime, false); std::string factionID = ""; if(arg0 >0) @@ -647,14 +648,7 @@ namespace MWScript } else { - if(ptr.getClass().getNpcStats(ptr).getFactionRanks().empty()) - { - factionID = ""; - } - else - { - factionID = ptr.getClass().getNpcStats(ptr).getFactionRanks().begin()->first; - } + factionID = ptr.getClass().getPrimaryFaction(ptr); } ::Misc::StringUtils::toLower(factionID); // Make sure this faction exists @@ -665,7 +659,7 @@ namespace MWScript { if(player.getClass().getNpcStats(player).getFactionRanks().find(factionID) != player.getClass().getNpcStats(player).getFactionRanks().end()) { - runtime.push(player.getClass().getNpcStats(player).getFactionRanks()[factionID]); + runtime.push(player.getClass().getNpcStats(player).getFactionRanks().at(factionID)); } else { @@ -750,6 +744,8 @@ namespace MWScript virtual void execute (Interpreter::Runtime& runtime, unsigned int arg0) { + MWWorld::Ptr ptr = R()(runtime, false); + std::string factionId; if (arg0==1) @@ -759,10 +755,7 @@ namespace MWScript } else { - MWWorld::Ptr ptr = R()(runtime); - - if (!ptr.getClass().getNpcStats (ptr).getFactionRanks().empty()) - factionId = ptr.getClass().getNpcStats (ptr).getFactionRanks().begin()->first; + factionId = getDialogueActorFaction(ptr); } if (factionId.empty()) @@ -783,6 +776,8 @@ namespace MWScript virtual void execute (Interpreter::Runtime& runtime, unsigned int arg0) { + MWWorld::Ptr ptr = R()(runtime, false); + Interpreter::Type_Integer value = runtime[0].mInteger; runtime.pop(); @@ -795,10 +790,7 @@ namespace MWScript } else { - MWWorld::Ptr ptr = R()(runtime); - - if (!ptr.getClass().getNpcStats (ptr).getFactionRanks().empty()) - factionId = ptr.getClass().getNpcStats (ptr).getFactionRanks().begin()->first; + factionId = getDialogueActorFaction(ptr); } if (factionId.empty()) @@ -818,6 +810,8 @@ namespace MWScript virtual void execute (Interpreter::Runtime& runtime, unsigned int arg0) { + MWWorld::Ptr ptr = R()(runtime, false); + Interpreter::Type_Integer value = runtime[0].mInteger; runtime.pop(); @@ -830,10 +824,7 @@ namespace MWScript } else { - MWWorld::Ptr ptr = R()(runtime); - - if (!ptr.getClass().getNpcStats (ptr).getFactionRanks().empty()) - factionId = ptr.getClass().getNpcStats (ptr).getFactionRanks().begin()->first; + factionId = getDialogueActorFaction(ptr); } if (factionId.empty()) @@ -913,7 +904,7 @@ namespace MWScript virtual void execute (Interpreter::Runtime& runtime, unsigned int arg0) { - MWWorld::Ptr ptr = R()(runtime); + MWWorld::Ptr ptr = R()(runtime, false); std::string factionID = ""; if(arg0 >0 ) @@ -923,14 +914,7 @@ namespace MWScript } else { - if(ptr.getClass().getNpcStats(ptr).getFactionRanks().empty()) - { - factionID = ""; - } - else - { - factionID = ptr.getClass().getNpcStats(ptr).getFactionRanks().begin()->first; - } + factionID = ptr.getClass().getPrimaryFaction(ptr); } ::Misc::StringUtils::toLower(factionID); MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); @@ -952,6 +936,8 @@ namespace MWScript virtual void execute (Interpreter::Runtime& runtime, unsigned int arg0) { + MWWorld::Ptr ptr = R()(runtime, false); + std::string factionID = ""; if(arg0 >0 ) { @@ -960,15 +946,7 @@ namespace MWScript } else { - MWWorld::Ptr ptr = R()(runtime); - if(ptr.getClass().getNpcStats(ptr).getFactionRanks().empty()) - { - factionID = ""; - } - else - { - factionID = ptr.getClass().getNpcStats(ptr).getFactionRanks().begin()->first; - } + factionID = ptr.getClass().getPrimaryFaction(ptr); } MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); if(factionID!="") @@ -985,6 +963,8 @@ namespace MWScript virtual void execute (Interpreter::Runtime& runtime, unsigned int arg0) { + MWWorld::Ptr ptr = R()(runtime, false); + std::string factionID = ""; if(arg0 >0 ) { @@ -993,15 +973,7 @@ namespace MWScript } else { - MWWorld::Ptr ptr = R()(runtime); - if(ptr.getClass().getNpcStats(ptr).getFactionRanks().empty()) - { - factionID = ""; - } - else - { - factionID = ptr.getClass().getNpcStats(ptr).getFactionRanks().begin()->first; - } + factionID = ptr.getClass().getPrimaryFaction(ptr); } MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); if(factionID!="") @@ -1018,21 +990,17 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - std::string factionID = ""; - if(ptr.getClass().getNpcStats(ptr).getFactionRanks().empty()) + std::string factionID = ptr.getClass().getPrimaryFaction(ptr); + if(factionID.empty()) return; - else - { - factionID = ptr.getClass().getNpcStats(ptr).getFactionRanks().begin()->first; - } + MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); // no-op when executed on the player if (ptr == player) return; - std::map& ranks = ptr.getClass().getNpcStats(ptr).getFactionRanks (); - ranks[factionID] = std::min(9, ranks[factionID]+1); + ptr.getClass().getNpcStats(ptr).raiseRank(factionID); } }; @@ -1045,21 +1013,17 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - std::string factionID = ""; - if(ptr.getClass().getNpcStats(ptr).getFactionRanks().empty()) + std::string factionID = ptr.getClass().getPrimaryFaction(ptr); + if(factionID.empty()) return; - else - { - factionID = ptr.getClass().getNpcStats(ptr).getFactionRanks().begin()->first; - } + MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); // no-op when executed on the player if (ptr == player) return; - std::map& ranks = ptr.getClass().getNpcStats(ptr).getFactionRanks (); - ranks[factionID] = std::max(0, ranks[factionID]-1); + ptr.getClass().getNpcStats(ptr).lowerRank(factionID); } }; @@ -1161,7 +1125,15 @@ namespace MWScript virtual void execute (Interpreter::Runtime& runtime) { MWWorld::Ptr ptr = R()(runtime); - ptr.getClass().getCreatureStats(ptr).resurrect(); + + if (ptr == MWBase::Environment::get().getWorld()->getPlayerPtr()) + ptr.getClass().getCreatureStats(ptr).resurrect(); + else if (ptr.getClass().getCreatureStats(ptr).isDead()) + { + MWBase::Environment::get().getWorld()->undeleteObject(ptr); + // resets runtime state such as inventory, stats and AI. does not reset position in the world + ptr.getRefData().setCustomData(NULL); + } } }; @@ -1176,6 +1148,91 @@ namespace MWScript } }; + template + class OpGetMagicEffect : public Interpreter::Opcode0 + { + int mPositiveEffect; + int mNegativeEffect; + + public: + OpGetMagicEffect (int positiveEffect, int negativeEffect) + : mPositiveEffect(positiveEffect) + , mNegativeEffect(negativeEffect) + { + } + + virtual void execute (Interpreter::Runtime& runtime) + { + MWWorld::Ptr ptr = R()(runtime); + + MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); + float currentValue = stats.getMagicEffects().get(mPositiveEffect).getMagnitude(); + if (mNegativeEffect != -1) + currentValue -= stats.getMagicEffects().get(mNegativeEffect).getMagnitude(); + + int ret = static_cast(currentValue); + runtime.push(ret); + } + }; + + template + class OpSetMagicEffect : public Interpreter::Opcode0 + { + int mPositiveEffect; + int mNegativeEffect; + + public: + OpSetMagicEffect (int positiveEffect, int negativeEffect) + : mPositiveEffect(positiveEffect) + , mNegativeEffect(negativeEffect) + { + } + + virtual void execute(Interpreter::Runtime &runtime) + { + MWWorld::Ptr ptr = R()(runtime); + MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); + float currentValue = stats.getMagicEffects().get(mPositiveEffect).getMagnitude(); + if (mNegativeEffect != -1) + currentValue -= stats.getMagicEffects().get(mNegativeEffect).getMagnitude(); + currentValue = int(currentValue); + + int arg = runtime[0].mInteger; + runtime.pop(); + stats.getMagicEffects().modifyBase(mPositiveEffect, (arg - currentValue)); + } + }; + + template + class OpModMagicEffect : public Interpreter::Opcode0 + { + int mPositiveEffect; + int mNegativeEffect; + + public: + OpModMagicEffect (int positiveEffect, int negativeEffect) + : mPositiveEffect(positiveEffect) + , mNegativeEffect(negativeEffect) + { + } + + virtual void execute(Interpreter::Runtime &runtime) + { + MWWorld::Ptr ptr = R()(runtime); + MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); + + int arg = runtime[0].mInteger; + runtime.pop(); + stats.getMagicEffects().modifyBase(mPositiveEffect, arg); + } + }; + + struct MagicEffect + { + int mPositiveEffect; + int mNegativeEffect; + }; + void installOpcodes (Interpreter::Interpreter& interpreter) { for (int i=0; i); interpreter.installSegment5 (Compiler::Stats::opcodeGetStat, new OpGetStat); interpreter.installSegment5 (Compiler::Stats::opcodeGetStatExplicit, new OpGetStat); + + static const MagicEffect sMagicEffects[] = { + { ESM::MagicEffect::ResistMagicka, ESM::MagicEffect::WeaknessToMagicka }, + { ESM::MagicEffect::ResistFire, ESM::MagicEffect::WeaknessToFire }, + { ESM::MagicEffect::ResistFrost, ESM::MagicEffect::WeaknessToFrost }, + { ESM::MagicEffect::ResistShock, ESM::MagicEffect::WeaknessToShock }, + { ESM::MagicEffect::ResistCommonDisease, ESM::MagicEffect::WeaknessToCommonDisease }, + { ESM::MagicEffect::ResistBlightDisease, ESM::MagicEffect::WeaknessToBlightDisease }, + { ESM::MagicEffect::ResistCorprusDisease, ESM::MagicEffect::WeaknessToCorprusDisease }, + { ESM::MagicEffect::ResistPoison, ESM::MagicEffect::WeaknessToPoison }, + { ESM::MagicEffect::ResistParalysis, -1 }, + { ESM::MagicEffect::ResistNormalWeapons, ESM::MagicEffect::WeaknessToNormalWeapons }, + { ESM::MagicEffect::WaterBreathing, -1 }, + { ESM::MagicEffect::Chameleon, -1 }, + { ESM::MagicEffect::WaterWalking, -1 }, + { ESM::MagicEffect::SwiftSwim, -1 }, + { ESM::MagicEffect::Jump, -1 }, + { ESM::MagicEffect::Levitate, -1 }, + { ESM::MagicEffect::Shield, -1 }, + { ESM::MagicEffect::Sound, -1 }, + { ESM::MagicEffect::Silence, -1 }, + { ESM::MagicEffect::Blind, -1 }, + { ESM::MagicEffect::Paralyze, -1 }, + { ESM::MagicEffect::Invisibility, -1 }, + { ESM::MagicEffect::FortifyAttack, -1 }, + { ESM::MagicEffect::Sanctuary, -1 }, + }; + + for (int i=0; i<24; ++i) + { + int positive = sMagicEffects[i].mPositiveEffect; + int negative = sMagicEffects[i].mNegativeEffect; + + interpreter.installSegment5 (Compiler::Stats::opcodeGetMagicEffect+i, new OpGetMagicEffect (positive, negative)); + interpreter.installSegment5 (Compiler::Stats::opcodeGetMagicEffectExplicit+i, new OpGetMagicEffect (positive, negative)); + + interpreter.installSegment5 (Compiler::Stats::opcodeSetMagicEffect+i, new OpSetMagicEffect (positive, negative)); + interpreter.installSegment5 (Compiler::Stats::opcodeSetMagicEffectExplicit+i, new OpSetMagicEffect (positive, negative)); + + interpreter.installSegment5 (Compiler::Stats::opcodeModMagicEffect+i, new OpModMagicEffect (positive, negative)); + interpreter.installSegment5 (Compiler::Stats::opcodeModMagicEffectExplicit+i, new OpModMagicEffect (positive, negative)); + } } } } diff --git a/apps/openmw/mwscript/transformationextensions.cpp b/apps/openmw/mwscript/transformationextensions.cpp index a041049ca..9166bf909 100644 --- a/apps/openmw/mwscript/transformationextensions.cpp +++ b/apps/openmw/mwscript/transformationextensions.cpp @@ -10,7 +10,9 @@ #include #include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" +#include "../mwworld/cellstore.hpp" #include "../mwworld/class.hpp" #include "../mwworld/manualref.hpp" #include "../mwworld/player.hpp" @@ -224,20 +226,23 @@ namespace MWScript float ay = ptr.getRefData().getPosition().pos[1]; float az = ptr.getRefData().getPosition().pos[2]; + MWWorld::Ptr updated = ptr; if(axis == "x") { - MWBase::Environment::get().getWorld()->moveObject(ptr,pos,ay,az); + updated = MWBase::Environment::get().getWorld()->moveObject(ptr,pos,ay,az); } else if(axis == "y") { - MWBase::Environment::get().getWorld()->moveObject(ptr,ax,pos,az); + updated = MWBase::Environment::get().getWorld()->moveObject(ptr,ax,pos,az); } else if(axis == "z") { - MWBase::Environment::get().getWorld()->moveObject(ptr,ax,ay,pos); + updated = MWBase::Environment::get().getWorld()->moveObject(ptr,ax,ay,pos); } else throw std::runtime_error ("invalid axis: " + axis); + + dynamic_cast(runtime.getContext()).updatePtr(updated); } }; @@ -305,33 +310,31 @@ namespace MWScript } catch(std::exception&) { - const ESM::Cell* cell = MWBase::Environment::get().getWorld()->getExterior(cellID); - if(cell) + const ESM::Cell* cell = MWBase::Environment::get().getWorld()->getExterior(cellID); + int cx,cy; + MWBase::Environment::get().getWorld()->positionToIndex(x,y,cx,cy); + store = MWBase::Environment::get().getWorld()->getExterior(cx,cy); + if(!cell) { - int cx,cy; - MWBase::Environment::get().getWorld()->positionToIndex(x,y,cx,cy); - store = MWBase::Environment::get().getWorld()->getExterior(cx,cy); + std::cerr << "unknown cell (" << cellID << ")\n"; } } if(store) { MWBase::Environment::get().getWorld()->moveObject(ptr,store,x,y,z); ptr = MWWorld::Ptr(ptr.getBase(), store); + dynamic_cast(runtime.getContext()).updatePtr(ptr); + float ax = Ogre::Radian(ptr.getRefData().getPosition().rot[0]).valueDegrees(); float ay = Ogre::Radian(ptr.getRefData().getPosition().rot[1]).valueDegrees(); - if(ptr.getTypeName() == typeid(ESM::NPC).name())//some morrowind oddity - { - ax = ax/60.; - ay = ay/60.; + // Note that you must specify ZRot in minutes (1 degree = 60 minutes; north = 0, east = 5400, south = 10800, west = 16200) + // except for when you position the player, then degrees must be used. + // See "Morrowind Scripting for Dummies (9th Edition)" pages 50 and 54 for reference. + if(ptr != MWBase::Environment::get().getWorld()->getPlayerPtr()) zRot = zRot/60.; - } MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,ay,zRot); - ptr.getClass().adjustPosition(ptr); - } - else - { - throw std::runtime_error (std::string("unknown cell (") + cellID + ")"); + ptr.getClass().adjustPosition(ptr, false); } } }; @@ -363,18 +366,31 @@ namespace MWScript runtime.pop(); int cx,cy; MWBase::Environment::get().getWorld()->positionToIndex(x,y,cx,cy); - MWBase::Environment::get().getWorld()->moveObject(ptr, - MWBase::Environment::get().getWorld()->getExterior(cx,cy),x,y,z); + + // another morrowind oddity: player will be moved to the exterior cell at this location, + // non-player actors will move within the cell they are in. + MWWorld::Ptr updated; + if (ptr.getRefData().getHandle() == "player") + { + MWWorld::CellStore* cell = MWBase::Environment::get().getWorld()->getExterior(cx,cy); + MWBase::Environment::get().getWorld()->moveObject(ptr,cell,x,y,z); + updated = MWWorld::Ptr(ptr.getBase(), cell); + } + else + { + updated = MWBase::Environment::get().getWorld()->moveObject(ptr, x, y, z); + } + dynamic_cast(runtime.getContext()).updatePtr(updated); + float ax = Ogre::Radian(ptr.getRefData().getPosition().rot[0]).valueDegrees(); float ay = Ogre::Radian(ptr.getRefData().getPosition().rot[1]).valueDegrees(); - if(ptr.getTypeName() == typeid(ESM::NPC).name())//some morrowind oddity - { - ax = ax/60.; - ay = ay/60.; + // Note that you must specify ZRot in minutes (1 degree = 60 minutes; north = 0, east = 5400, south = 10800, west = 16200) + // except for when you position the player, then degrees must be used. + // See "Morrowind Scripting for Dummies (9th Edition)" pages 50 and 54 for reference. + if(ptr != MWBase::Environment::get().getWorld()->getPlayerPtr()) zRot = zRot/60.; - } MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,ay,zRot); - ptr.getClass().adjustPosition(ptr); + ptr.getClass().adjustPosition(ptr, false); } }; @@ -407,11 +423,12 @@ namespace MWScript catch(std::exception&) { const ESM::Cell* cell = MWBase::Environment::get().getWorld()->getExterior(cellID); - if(cell) + int cx,cy; + MWBase::Environment::get().getWorld()->positionToIndex(x,y,cx,cy); + store = MWBase::Environment::get().getWorld()->getExterior(cx,cy); + if(!cell) { - int cx,cy; - MWBase::Environment::get().getWorld()->positionToIndex(x,y,cx,cy); - store = MWBase::Environment::get().getWorld()->getExterior(cx,cy); + std::cerr << "unknown cell (" << cellID << ")\n"; } } if(store) @@ -424,11 +441,8 @@ namespace MWScript pos.rot[2] = zRot; MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(),itemID); ref.getPtr().getCellRef().setPosition(pos); - MWBase::Environment::get().getWorld()->safePlaceObject(ref.getPtr(),store,pos); - } - else - { - throw std::runtime_error ( std::string("unknown cell (") + cellID + ")"); + MWWorld::Ptr placed = MWBase::Environment::get().getWorld()->safePlaceObject(ref.getPtr(),store,pos); + placed.getClass().adjustPosition(placed, true); } } }; @@ -452,25 +466,27 @@ namespace MWScript Interpreter::Type_Float zRot = runtime[0].mFloat; runtime.pop(); - int cx,cy; - MWBase::Environment::get().getWorld()->positionToIndex(x,y,cx,cy); - MWWorld::CellStore* store = MWBase::Environment::get().getWorld()->getExterior(cx,cy); - if(store) + MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); + MWWorld::CellStore* store = NULL; + if (player.getCell()->isExterior()) { - ESM::Position pos; - pos.pos[0] = x; - pos.pos[1] = y; - pos.pos[2] = z; - pos.rot[0] = pos.rot[1] = 0; - pos.rot[2] = zRot; - MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(),itemID); - ref.getPtr().getCellRef().setPosition(pos); - MWBase::Environment::get().getWorld()->safePlaceObject(ref.getPtr(),store,pos); + int cx,cy; + MWBase::Environment::get().getWorld()->positionToIndex(x,y,cx,cy); + store = MWBase::Environment::get().getWorld()->getExterior(cx,cy); } else - { - throw std::runtime_error ("unknown cell"); - } + store = player.getCell(); + + ESM::Position pos; + pos.pos[0] = x; + pos.pos[1] = y; + pos.pos[2] = z; + pos.rot[0] = pos.rot[1] = 0; + pos.rot[2] = zRot; + MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(),itemID); + ref.getPtr().getCellRef().setPosition(pos); + MWWorld::Ptr placed = MWBase::Environment::get().getWorld()->safePlaceObject(ref.getPtr(),store,pos); + placed.getClass().adjustPosition(placed, true); } }; @@ -498,42 +514,41 @@ namespace MWScript if (count<0) throw std::runtime_error ("count must be non-negative"); - // no-op - if (count == 0) - return; + for (int i=0; igetStore(), itemID, 1); + ref.getPtr().getCellRef().setPosition(ipos); - if (actor.getClass().isActor()) - { - // TODO: should this depend on the 'direction' parameter? - ipos.rot[0] = 0; - ipos.rot[1] = 0; - ipos.rot[2] = 0; - } - else - { - ipos.rot[0] = actor.getRefData().getPosition().rot[0]; - ipos.rot[1] = actor.getRefData().getPosition().rot[1]; - ipos.rot[2] = actor.getRefData().getPosition().rot[2]; + MWBase::Environment::get().getWorld()->safePlaceObject(ref.getPtr(),store,ipos); } - // create item - MWWorld::CellStore* store = actor.getCell(); - MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), itemID, count); - ref.getPtr().getCellRef().setPosition(ipos); - - MWBase::Environment::get().getWorld()->safePlaceObject(ref.getPtr(),store,ipos); } }; @@ -628,8 +643,10 @@ namespace MWScript ptr.getRefData().setLocalRotation(rot); MWBase::Environment::get().getWorld()->rotateObject(ptr, 0,0,0,true); - MWBase::Environment::get().getWorld()->moveObject(ptr, ptr.getCellRef().getPosition().pos[0], - ptr.getCellRef().getPosition().pos[1], ptr.getCellRef().getPosition().pos[2]); + + dynamic_cast(runtime.getContext()).updatePtr( + MWBase::Environment::get().getWorld()->moveObject(ptr, ptr.getCellRef().getPosition().pos[0], + ptr.getCellRef().getPosition().pos[1], ptr.getCellRef().getPosition().pos[2])); } }; @@ -667,7 +684,12 @@ namespace MWScript else throw std::runtime_error ("invalid movement axis: " + axis); - Ogre::Vector3 worldPos = ptr.getRefData().getBaseNode()->convertLocalToWorldPosition(posChange); + if (!ptr.getRefData().getBaseNode()) + return; + + Ogre::Vector3 diff = ptr.getRefData().getBaseNode()->getOrientation() * posChange; + Ogre::Vector3 worldPos(ptr.getRefData().getPosition().pos); + worldPos += diff; MWBase::Environment::get().getWorld()->moveObject(ptr, worldPos.x, worldPos.y, worldPos.z); } }; @@ -691,23 +713,33 @@ namespace MWScript const float *objPos = ptr.getRefData().getPosition().pos; + MWWorld::Ptr updated; if (axis == "x") { - MWBase::Environment::get().getWorld()->moveObject(ptr, objPos[0]+movement, objPos[1], objPos[2]); + updated = MWBase::Environment::get().getWorld()->moveObject(ptr, objPos[0]+movement, objPos[1], objPos[2]); } else if (axis == "y") { - MWBase::Environment::get().getWorld()->moveObject(ptr, objPos[0], objPos[1]+movement, objPos[2]); + updated = MWBase::Environment::get().getWorld()->moveObject(ptr, objPos[0], objPos[1]+movement, objPos[2]); } else if (axis == "z") { - MWBase::Environment::get().getWorld()->moveObject(ptr, objPos[0], objPos[1], objPos[2]+movement); + updated = MWBase::Environment::get().getWorld()->moveObject(ptr, objPos[0], objPos[1], objPos[2]+movement); } else throw std::runtime_error ("invalid movement axis: " + axis); } }; + class OpResetActors : public Interpreter::Opcode0 + { + public: + + virtual void execute (Interpreter::Runtime& runtime) + { + MWBase::Environment::get().getWorld()->resetActors(); + } + }; void installOpcodes (Interpreter::Interpreter& interpreter) { @@ -748,6 +780,7 @@ namespace MWScript interpreter.installSegment5(Compiler::Transformation::opcodeMoveWorldExplicit,new OpMoveWorld); interpreter.installSegment5(Compiler::Transformation::opcodeGetStartingAngle, new OpGetStartingAngle); interpreter.installSegment5(Compiler::Transformation::opcodeGetStartingAngleExplicit, new OpGetStartingAngle); + interpreter.installSegment5(Compiler::Transformation::opcodeResetActors, new OpResetActors); } } } diff --git a/apps/openmw/mwsound/ffmpeg_decoder.cpp b/apps/openmw/mwsound/ffmpeg_decoder.cpp index 10a782b96..bc467acff 100644 --- a/apps/openmw/mwsound/ffmpeg_decoder.cpp +++ b/apps/openmw/mwsound/ffmpeg_decoder.cpp @@ -1,6 +1,3 @@ -#ifdef OPENMW_USE_FFMPEG - - #include "ffmpeg_decoder.hpp" // auto_ptr @@ -8,6 +5,16 @@ #include +extern "C" { +#ifndef HAVE_LIBSWRESAMPLE +// FIXME: remove this section once libswresample is packaged for Debian +int swr_init(AVAudioResampleContext *avr); +void swr_free(AVAudioResampleContext **avr); +int swr_convert( AVAudioResampleContext *avr, uint8_t** output, int out_samples, const uint8_t** input, int in_samples); +AVAudioResampleContext * swr_alloc_set_opts( AVAudioResampleContext *avr, int64_t out_ch_layout, AVSampleFormat out_fmt, int out_rate, int64_t in_ch_layout, AVSampleFormat in_fmt, int in_rate, int o, void* l); +#endif +} + namespace MWSound { @@ -18,14 +25,28 @@ void FFmpeg_Decoder::fail(const std::string &msg) int FFmpeg_Decoder::readPacket(void *user_data, uint8_t *buf, int buf_size) { - Ogre::DataStreamPtr stream = static_cast(user_data)->mDataStream; - return stream->read(buf, buf_size); + try + { + Ogre::DataStreamPtr stream = static_cast(user_data)->mDataStream; + return stream->read(buf, buf_size); + } + catch (std::exception& e) + { + return 0; + } } int FFmpeg_Decoder::writePacket(void *user_data, uint8_t *buf, int buf_size) { - Ogre::DataStreamPtr stream = static_cast(user_data)->mDataStream; - return stream->write(buf, buf_size); + try + { + Ogre::DataStreamPtr stream = static_cast(user_data)->mDataStream; + return stream->write(buf, buf_size); + } + catch (std::exception& e) + { + return 0; + } } int64_t FFmpeg_Decoder::seek(void *user_data, int64_t offset, int whence) @@ -62,7 +83,7 @@ bool FFmpeg_Decoder::getNextPacket() /* Check if the packet belongs to this stream */ if(stream_idx == mPacket.stream_index) { - if((uint64_t)mPacket.pts != AV_NOPTS_VALUE) + if(mPacket.pts != (int64_t)AV_NOPTS_VALUE) mNextPts = av_q2d((*mStream)->time_base)*mPacket.pts; return true; } @@ -76,7 +97,7 @@ bool FFmpeg_Decoder::getNextPacket() bool FFmpeg_Decoder::getAVAudioData() { - int got_frame, len; + int got_frame; if((*mStream)->codec->codec_type != AVMEDIA_TYPE_AUDIO) return false; @@ -86,6 +107,7 @@ bool FFmpeg_Decoder::getAVAudioData() return false; /* Decode some data, and check for errors */ + int len = 0; if((len=avcodec_decode_audio4((*mStream)->codec, mFrame, &got_frame, &mPacket)) < 0) return false; @@ -98,6 +120,32 @@ bool FFmpeg_Decoder::getAVAudioData() memmove(mPacket.data, &mPacket.data[len], remaining); av_shrink_packet(&mPacket, remaining); } + + if (!got_frame || mFrame->nb_samples == 0) + continue; + + if(mSwr) + { + if(!mDataBuf || mDataBufLen < mFrame->nb_samples) + { + av_freep(&mDataBuf); + if(av_samples_alloc(&mDataBuf, NULL, av_get_channel_layout_nb_channels(mOutputChannelLayout), + mFrame->nb_samples, mOutputSampleFormat, 0) < 0) + return false; + else + mDataBufLen = mFrame->nb_samples; + } + + if(swr_convert(mSwr, (uint8_t**)&mDataBuf, mFrame->nb_samples, + (const uint8_t**)mFrame->extended_data, mFrame->nb_samples) < 0) + { + return false; + } + mFrameData = &mDataBuf; + } + else + mFrameData = &mFrame->data[0]; + } while(got_frame == 0 || mFrame->nb_samples == 0); mNextPts += (double)mFrame->nb_samples / (double)(*mStream)->codec->sample_rate; @@ -116,8 +164,8 @@ size_t FFmpeg_Decoder::readAVAudioData(void *data, size_t length) if(!getAVAudioData()) break; mFramePos = 0; - mFrameSize = mFrame->nb_samples * (*mStream)->codec->channels * - av_get_bytes_per_sample((*mStream)->codec->sample_fmt); + mFrameSize = mFrame->nb_samples * av_get_channel_layout_nb_channels(mOutputChannelLayout) * + av_get_bytes_per_sample(mOutputSampleFormat); } /* Get the amount of bytes remaining to be written, and clamp to @@ -125,7 +173,7 @@ size_t FFmpeg_Decoder::readAVAudioData(void *data, size_t length) size_t rem = std::min(length-dec, mFrameSize-mFramePos); /* Copy the data to the app's buffer and increment */ - memcpy(data, mFrame->data[0]+mFramePos, rem); + memcpy(data, mFrameData[0]+mFramePos, rem); data = (char*)data + rem; dec += rem; mFramePos += rem; @@ -135,19 +183,6 @@ size_t FFmpeg_Decoder::readAVAudioData(void *data, size_t length) return dec; } -static AVSampleFormat ffmpegNonPlanarSampleFormat (AVSampleFormat format) -{ - switch (format) - { - case AV_SAMPLE_FMT_U8P: return AV_SAMPLE_FMT_U8; - case AV_SAMPLE_FMT_S16P: return AV_SAMPLE_FMT_S16; - case AV_SAMPLE_FMT_S32P: return AV_SAMPLE_FMT_S32; - case AV_SAMPLE_FMT_FLTP: return AV_SAMPLE_FMT_FLT; - case AV_SAMPLE_FMT_DBLP: return AV_SAMPLE_FMT_DBL; - default:return format; - } -} - void FFmpeg_Decoder::open(const std::string &fname) { close(); @@ -194,7 +229,7 @@ void FFmpeg_Decoder::open(const std::string &fname) if(!mStream) fail("No audio streams in "+fname); - (*mStream)->codec->request_sample_fmt = ffmpegNonPlanarSampleFormat ((*mStream)->codec->sample_fmt); + (*mStream)->codec->request_sample_fmt = (*mStream)->codec->sample_fmt; AVCodec *codec = avcodec_find_decoder((*mStream)->codec->codec_id); if(!codec) @@ -206,7 +241,7 @@ void FFmpeg_Decoder::open(const std::string &fname) if(avcodec_open2((*mStream)->codec, codec, NULL) < 0) fail("Failed to open audio codec " + std::string(codec->long_name)); - mFrame = avcodec_alloc_frame(); + mFrame = av_frame_alloc(); } catch(std::exception&) { @@ -231,6 +266,8 @@ void FFmpeg_Decoder::close() av_free_packet(&mPacket); av_freep(&mFrame); + swr_free(&mSwr); + av_freep(&mDataBuf); if(mFormatCtx) { @@ -265,40 +302,45 @@ void FFmpeg_Decoder::getInfo(int *samplerate, ChannelConfig *chans, SampleType * if(!mStream) fail("No audio stream info"); - if((*mStream)->codec->sample_fmt == AV_SAMPLE_FMT_U8) + if((*mStream)->codec->sample_fmt == AV_SAMPLE_FMT_FLT || (*mStream)->codec->sample_fmt == AV_SAMPLE_FMT_FLTP) + mOutputSampleFormat = AV_SAMPLE_FMT_S16; // FIXME: Check for AL_EXT_FLOAT32 support + else if((*mStream)->codec->sample_fmt == AV_SAMPLE_FMT_U8P) + mOutputSampleFormat = AV_SAMPLE_FMT_U8; + else if((*mStream)->codec->sample_fmt == AV_SAMPLE_FMT_S16P) + mOutputSampleFormat = AV_SAMPLE_FMT_S16; + else + mOutputSampleFormat = AV_SAMPLE_FMT_S16; + + if(mOutputSampleFormat == AV_SAMPLE_FMT_U8) *type = SampleType_UInt8; - else if((*mStream)->codec->sample_fmt == AV_SAMPLE_FMT_S16) + else if(mOutputSampleFormat == AV_SAMPLE_FMT_S16) *type = SampleType_Int16; - else if((*mStream)->codec->sample_fmt == AV_SAMPLE_FMT_FLT) + else if(mOutputSampleFormat == AV_SAMPLE_FMT_FLT) *type = SampleType_Float32; - else - fail(std::string("Unsupported sample format: ")+ - av_get_sample_fmt_name((*mStream)->codec->sample_fmt)); - if((*mStream)->codec->channel_layout == AV_CH_LAYOUT_MONO) + int64_t ch_layout = (*mStream)->codec->channel_layout; + + if(ch_layout == 0) + ch_layout = av_get_default_channel_layout((*mStream)->codec->channels); + + mOutputChannelLayout = ch_layout; + if (ch_layout == AV_CH_LAYOUT_5POINT1 || ch_layout == AV_CH_LAYOUT_7POINT1 + || ch_layout == AV_CH_LAYOUT_QUAD) // FIXME: check for AL_EXT_MCFORMATS support + mOutputChannelLayout = AV_CH_LAYOUT_STEREO; + else if (ch_layout != AV_CH_LAYOUT_MONO + && ch_layout != AV_CH_LAYOUT_STEREO) + mOutputChannelLayout = AV_CH_LAYOUT_STEREO; + + if(mOutputChannelLayout == AV_CH_LAYOUT_MONO) *chans = ChannelConfig_Mono; - else if((*mStream)->codec->channel_layout == AV_CH_LAYOUT_STEREO) + else if(mOutputChannelLayout == AV_CH_LAYOUT_STEREO) *chans = ChannelConfig_Stereo; - else if((*mStream)->codec->channel_layout == AV_CH_LAYOUT_QUAD) + else if(mOutputChannelLayout == AV_CH_LAYOUT_QUAD) *chans = ChannelConfig_Quad; - else if((*mStream)->codec->channel_layout == AV_CH_LAYOUT_5POINT1) + else if(mOutputChannelLayout == AV_CH_LAYOUT_5POINT1) *chans = ChannelConfig_5point1; - else if((*mStream)->codec->channel_layout == AV_CH_LAYOUT_7POINT1) + else if(mOutputChannelLayout == AV_CH_LAYOUT_7POINT1) *chans = ChannelConfig_7point1; - else if((*mStream)->codec->channel_layout == 0) - { - /* Unknown channel layout. Try to guess. */ - if((*mStream)->codec->channels == 1) - *chans = ChannelConfig_Mono; - else if((*mStream)->codec->channels == 2) - *chans = ChannelConfig_Stereo; - else - { - std::stringstream sstr("Unsupported raw channel count: "); - sstr << (*mStream)->codec->channels; - fail(sstr.str()); - } - } else { char str[1024]; @@ -308,6 +350,25 @@ void FFmpeg_Decoder::getInfo(int *samplerate, ChannelConfig *chans, SampleType * } *samplerate = (*mStream)->codec->sample_rate; + + if(mOutputSampleFormat != (*mStream)->codec->sample_fmt + || mOutputChannelLayout != ch_layout) + { + mSwr = swr_alloc_set_opts(mSwr, // SwrContext + mOutputChannelLayout, // output ch layout + mOutputSampleFormat, // output sample format + (*mStream)->codec->sample_rate, // output sample rate + ch_layout, // input ch layout + (*mStream)->codec->sample_fmt, // input sample format + (*mStream)->codec->sample_rate, // input sample rate + 0, // logging level offset + NULL); // log context + if(!mSwr) + fail(std::string("Couldn't allocate SwrContext")); + if(swr_init(mSwr) < 0) + fail(std::string("Couldn't initialize SwrContext")); + + } } size_t FFmpeg_Decoder::read(char *buffer, size_t bytes) @@ -324,9 +385,9 @@ void FFmpeg_Decoder::readAll(std::vector &output) while(getAVAudioData()) { - size_t got = mFrame->nb_samples * (*mStream)->codec->channels * - av_get_bytes_per_sample((*mStream)->codec->sample_fmt); - const char *inbuf = reinterpret_cast(mFrame->data[0]); + size_t got = mFrame->nb_samples * av_get_channel_layout_nb_channels(mOutputChannelLayout) * + av_get_bytes_per_sample(mOutputSampleFormat); + const char *inbuf = reinterpret_cast(mFrameData[0]); output.insert(output.end(), inbuf, inbuf+got); } } @@ -343,8 +404,8 @@ void FFmpeg_Decoder::rewind() size_t FFmpeg_Decoder::getSampleOffset() { - int delay = (mFrameSize-mFramePos) / (*mStream)->codec->channels / - av_get_bytes_per_sample((*mStream)->codec->sample_fmt); + int delay = (mFrameSize-mFramePos) / av_get_channel_layout_nb_channels(mOutputChannelLayout) / + av_get_bytes_per_sample(mOutputSampleFormat); return (int)(mNextPts*(*mStream)->codec->sample_rate) - delay; } @@ -355,6 +416,12 @@ FFmpeg_Decoder::FFmpeg_Decoder() , mFrameSize(0) , mFramePos(0) , mNextPts(0.0) + , mSwr(0) + , mOutputSampleFormat(AV_SAMPLE_FMT_NONE) + , mOutputChannelLayout(0) + , mDataBuf(NULL) + , mFrameData(NULL) + , mDataBufLen(0) { memset(&mPacket, 0, sizeof(mPacket)); @@ -375,5 +442,3 @@ FFmpeg_Decoder::~FFmpeg_Decoder() } } - -#endif diff --git a/apps/openmw/mwsound/ffmpeg_decoder.hpp b/apps/openmw/mwsound/ffmpeg_decoder.hpp index 8276b45c7..2cdbbf363 100644 --- a/apps/openmw/mwsound/ffmpeg_decoder.hpp +++ b/apps/openmw/mwsound/ffmpeg_decoder.hpp @@ -4,7 +4,9 @@ // FIXME: This can't be right? The headers refuse to build without UINT64_C, // which only gets defined in stdint.h in either C99 mode or with this macro // defined... +#ifndef __STDC_CONSTANT_MACROS #define __STDC_CONSTANT_MACROS +#endif #include extern "C" { @@ -18,6 +20,21 @@ extern "C" LIBAVUTIL_VERSION_MINOR, LIBAVUTIL_VERSION_MICRO) #include #endif + +#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(55,28,1) +#define av_frame_alloc avcodec_alloc_frame +#endif + +// From version 54.56 binkaudio encoding format changed from S16 to FLTP. See: +// https://gitorious.org/ffmpeg/ffmpeg/commit/7bfd1766d1c18f07b0a2dd042418a874d49ea60d +// http://ffmpeg.zeranoe.com/forum/viewtopic.php?f=15&t=872 +#ifdef HAVE_LIBSWRESAMPLE +#include +#else +#include +#include +#define SwrContext AVAudioResampleContext +#endif } #include @@ -40,6 +57,13 @@ namespace MWSound double mNextPts; + SwrContext *mSwr; + enum AVSampleFormat mOutputSampleFormat; + int64_t mOutputChannelLayout; + uint8_t *mDataBuf; + uint8_t **mFrameData; + int mDataBufLen; + bool getNextPacket(); Ogre::DataStreamPtr mDataStream; diff --git a/apps/openmw/mwsound/libavwrapper.cpp b/apps/openmw/mwsound/libavwrapper.cpp new file mode 100644 index 000000000..a7a3245da --- /dev/null +++ b/apps/openmw/mwsound/libavwrapper.cpp @@ -0,0 +1,111 @@ +#ifndef HAVE_LIBSWRESAMPLE +extern "C" +{ +#ifndef __STDC_CONSTANT_MACROS +#define __STDC_CONSTANT_MACROS +#endif +#include + +#include +#include +// From libavutil version 52.2.0 and onward the declaration of +// AV_CH_LAYOUT_* is removed from libavcodec/avcodec.h and moved to +// libavutil/channel_layout.h +#if AV_VERSION_INT(52, 2, 0) <= AV_VERSION_INT(LIBAVUTIL_VERSION_MAJOR, \ + LIBAVUTIL_VERSION_MINOR, LIBAVUTIL_VERSION_MICRO) + #include +#endif +#include +#include + +/* FIXME: delete this file once libswresample is packaged for Debian */ + +int swr_init(AVAudioResampleContext *avr) { return 1; } + +void swr_free(AVAudioResampleContext **avr) { avresample_free(avr); } + +int swr_convert( + AVAudioResampleContext *avr, + uint8_t** output, + int out_samples, + const uint8_t** input, + int in_samples) +{ + // FIXME: potential performance hit + int out_plane_size = 0; + int in_plane_size = 0; + return avresample_convert(avr, output, out_plane_size, out_samples, + (uint8_t **)input, in_plane_size, in_samples); +} + +AVAudioResampleContext * swr_alloc_set_opts( + AVAudioResampleContext *avr, + int64_t out_ch_layout, + AVSampleFormat out_fmt, + int out_rate, + int64_t in_ch_layout, + AVSampleFormat in_fmt, + int in_rate, + int o, + void* l) +{ + avr = avresample_alloc_context(); + if(!avr) + return 0; + + int res; + res = av_opt_set_int(avr, "out_channel_layout", out_ch_layout, 0); + if(res < 0) + { + av_log(avr, AV_LOG_ERROR, "av_opt_set_int: out_ch_layout = %d\n", res); + return 0; + } + res = av_opt_set_int(avr, "out_sample_fmt", out_fmt, 0); + if(res < 0) + { + av_log(avr, AV_LOG_ERROR, "av_opt_set_int: out_fmt = %d\n", res); + return 0; + } + res = av_opt_set_int(avr, "out_sample_rate", out_rate, 0); + if(res < 0) + { + av_log(avr, AV_LOG_ERROR, "av_opt_set_int: out_rate = %d\n", res); + return 0; + } + res = av_opt_set_int(avr, "in_channel_layout", in_ch_layout, 0); + if(res < 0) + { + av_log(avr, AV_LOG_ERROR, "av_opt_set_int: in_ch_layout = %d\n", res); + return 0; + } + res = av_opt_set_int(avr, "in_sample_fmt", in_fmt, 0); + if(res < 0) + { + av_log(avr, AV_LOG_ERROR, "av_opt_set_int: in_fmt = %d\n", res); + return 0; + } + res = av_opt_set_int(avr, "in_sample_rate", in_rate, 0); + if(res < 0) + { + av_log(avr, AV_LOG_ERROR, "av_opt_set_int: in_rate = %d\n", res); + return 0; + } + res = av_opt_set_int(avr, "internal_sample_fmt", AV_SAMPLE_FMT_FLTP, 0); + if(res < 0) + { + av_log(avr, AV_LOG_ERROR, "av_opt_set_int: internal_sample_fmt\n"); + return 0; + } + + + if(avresample_open(avr) < 0) + { + av_log(avr, AV_LOG_ERROR, "Error opening context\n"); + return 0; + } + else + return avr; +} + +} +#endif diff --git a/apps/openmw/mwsound/loudness.cpp b/apps/openmw/mwsound/loudness.cpp new file mode 100644 index 000000000..c64913b1f --- /dev/null +++ b/apps/openmw/mwsound/loudness.cpp @@ -0,0 +1,53 @@ +#include "loudness.hpp" + +#include "soundmanagerimp.hpp" + +namespace MWSound +{ + + void analyzeLoudness(const std::vector &data, int sampleRate, ChannelConfig chans, + SampleType type, std::vector &out, float valuesPerSecond) + { + int samplesPerSegment = sampleRate / valuesPerSecond; + int numSamples = bytesToFrames(data.size(), chans, type); + int advance = framesToBytes(1, chans, type); + + out.reserve(numSamples/samplesPerSegment); + + int segment=0; + int sample=0; + while (segment < numSamples/samplesPerSegment) + { + float sum=0; + int samplesAdded = 0; + while (sample < numSamples && sample < (segment+1)*samplesPerSegment) + { + // get sample on a scale from -1 to 1 + float value = 0; + if (type == SampleType_UInt8) + value = ((char)(data[sample*advance]^0x80))/128.f; + else if (type == SampleType_Int16) + { + value = *reinterpret_cast(&data[sample*advance]); + value /= float(std::numeric_limits::max()); + } + else if (type == SampleType_Float32) + { + value = *reinterpret_cast(&data[sample*advance]); + value = std::max(-1.f, std::min(1.f, value)); // Float samples *should* be scaled to [-1,1] already. + } + + sum += value*value; + ++samplesAdded; + ++sample; + } + + float rms = 0; // root mean square + if (samplesAdded > 0) + rms = std::sqrt(sum / samplesAdded); + out.push_back(rms); + ++segment; + } + } + +} diff --git a/apps/openmw/mwsound/loudness.hpp b/apps/openmw/mwsound/loudness.hpp new file mode 100644 index 000000000..df727bd0b --- /dev/null +++ b/apps/openmw/mwsound/loudness.hpp @@ -0,0 +1,20 @@ +#include "sound_decoder.hpp" + +namespace MWSound +{ + +/** + * Analyzes the energy (closely related to loudness) of a sound buffer. + * The buffer will be divided into segments according to \a valuesPerSecond, + * and for each segment a loudness value in the range of [0,1] will be computed. + * @param data the sound buffer to analyze, containing raw samples + * @param sampleRate the sample rate of the sound buffer + * @param chans channel layout of the buffer + * @param type sample type of the buffer + * @param out Will contain the output loudness values. + * @param valuesPerSecond How many loudness values per second of audio to compute. + */ +void analyzeLoudness (const std::vector& data, int sampleRate, ChannelConfig chans, SampleType type, + std::vector& out, float valuesPerSecond); + +} diff --git a/apps/openmw/mwsound/movieaudiofactory.cpp b/apps/openmw/mwsound/movieaudiofactory.cpp new file mode 100644 index 000000000..468f8c82c --- /dev/null +++ b/apps/openmw/mwsound/movieaudiofactory.cpp @@ -0,0 +1,173 @@ +#include "movieaudiofactory.hpp" + +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/soundmanager.hpp" + +#include "sound_decoder.hpp" +#include "sound.hpp" + +namespace MWSound +{ + + class MovieAudioDecoder; + class MWSoundDecoderBridge : public Sound_Decoder + { + public: + MWSoundDecoderBridge(MWSound::MovieAudioDecoder* decoder) + : mDecoder(decoder) + { + } + + private: + MWSound::MovieAudioDecoder* mDecoder; + + virtual void open(const std::string &fname); + virtual void close(); + virtual void rewind(); + virtual std::string getName(); + virtual void getInfo(int *samplerate, ChannelConfig *chans, SampleType *type); + virtual size_t read(char *buffer, size_t bytes); + virtual size_t getSampleOffset(); + }; + + class MovieAudioDecoder : public Video::MovieAudioDecoder + { + public: + MovieAudioDecoder(Video::VideoState *videoState) + : Video::MovieAudioDecoder(videoState) + { + mDecoderBridge.reset(new MWSoundDecoderBridge(this)); + } + + size_t getSampleOffset() + { + ssize_t clock_delay = (mFrameSize-mFramePos) / av_get_channel_layout_nb_channels(mOutputChannelLayout) / + av_get_bytes_per_sample(mOutputSampleFormat); + return (size_t)(mAudioClock*mAVStream->codec->sample_rate) - clock_delay; + } + + std::string getStreamName() + { + return mVideoState->stream->getName(); + } + + private: + // MovieAudioDecoder overrides + + virtual double getAudioClock() + { + return mAudioTrack->getTimeOffset(); + } + + virtual void adjustAudioSettings(AVSampleFormat& sampleFormat, uint64_t& channelLayout, int& sampleRate) + { + if (sampleFormat == AV_SAMPLE_FMT_U8P || sampleFormat == AV_SAMPLE_FMT_U8) + sampleFormat = AV_SAMPLE_FMT_U8; + else if (sampleFormat == AV_SAMPLE_FMT_S16P || sampleFormat == AV_SAMPLE_FMT_S16) + sampleFormat = AV_SAMPLE_FMT_S16; + else if (sampleFormat == AV_SAMPLE_FMT_FLTP || sampleFormat == AV_SAMPLE_FMT_FLT) + sampleFormat = AV_SAMPLE_FMT_S16; // FIXME: check for AL_EXT_FLOAT32 support + else + sampleFormat = AV_SAMPLE_FMT_S16; + + if (channelLayout == AV_CH_LAYOUT_5POINT1 || channelLayout == AV_CH_LAYOUT_7POINT1 + || channelLayout == AV_CH_LAYOUT_QUAD) // FIXME: check for AL_EXT_MCFORMATS support + channelLayout = AV_CH_LAYOUT_STEREO; + else if (channelLayout != AV_CH_LAYOUT_MONO + && channelLayout != AV_CH_LAYOUT_STEREO) + channelLayout = AV_CH_LAYOUT_STEREO; + } + + public: + ~MovieAudioDecoder() + { + mAudioTrack.reset(); + mDecoderBridge.reset(); + } + + MWBase::SoundPtr mAudioTrack; + boost::shared_ptr mDecoderBridge; + }; + + + void MWSoundDecoderBridge::open(const std::string &fname) + { + throw std::runtime_error("unimplemented"); + } + void MWSoundDecoderBridge::close() {} + void MWSoundDecoderBridge::rewind() {} + + std::string MWSoundDecoderBridge::getName() + { + return mDecoder->getStreamName(); + } + + void MWSoundDecoderBridge::getInfo(int *samplerate, ChannelConfig *chans, SampleType *type) + { + *samplerate = mDecoder->getOutputSampleRate(); + + uint64_t outputChannelLayout = mDecoder->getOutputChannelLayout(); + if (outputChannelLayout == AV_CH_LAYOUT_MONO) + *chans = ChannelConfig_Mono; + else if (outputChannelLayout == AV_CH_LAYOUT_5POINT1) + *chans = ChannelConfig_5point1; + else if (outputChannelLayout == AV_CH_LAYOUT_7POINT1) + *chans = ChannelConfig_7point1; + else if (outputChannelLayout == AV_CH_LAYOUT_STEREO) + *chans = ChannelConfig_Stereo; + else if (outputChannelLayout == AV_CH_LAYOUT_QUAD) + *chans = ChannelConfig_Quad; + else + { + std::stringstream error; + error << "Unsupported channel layout: " << outputChannelLayout; + throw std::runtime_error(error.str()); + } + + AVSampleFormat outputSampleFormat = mDecoder->getOutputSampleFormat(); + if (outputSampleFormat == AV_SAMPLE_FMT_U8) + *type = SampleType_UInt8; + else if (outputSampleFormat == AV_SAMPLE_FMT_FLT) + *type = SampleType_Float32; + else if (outputSampleFormat == AV_SAMPLE_FMT_S16) + *type = SampleType_Int16; + else + { + char str[1024]; + av_get_sample_fmt_string(str, sizeof(str), outputSampleFormat); + throw std::runtime_error(std::string("Unsupported sample format: ") + str); + } + } + + size_t MWSoundDecoderBridge::read(char *buffer, size_t bytes) + { + return mDecoder->read(buffer, bytes); + } + + size_t MWSoundDecoderBridge::getSampleOffset() + { + return mDecoder->getSampleOffset(); + } + + + + boost::shared_ptr MovieAudioFactory::createDecoder(Video::VideoState* videoState) + { + boost::shared_ptr decoder(new MWSound::MovieAudioDecoder(videoState)); + decoder->setupFormat(); + + MWBase::SoundPtr sound = MWBase::Environment::get().getSoundManager()->playTrack(decoder->mDecoderBridge, MWBase::SoundManager::Play_TypeMovie); + if (!sound.get()) + { + decoder.reset(); + return decoder; + } + + decoder->mAudioTrack = sound; + return decoder; + } + +} diff --git a/apps/openmw/mwsound/movieaudiofactory.hpp b/apps/openmw/mwsound/movieaudiofactory.hpp new file mode 100644 index 000000000..a3c602197 --- /dev/null +++ b/apps/openmw/mwsound/movieaudiofactory.hpp @@ -0,0 +1,16 @@ +#ifndef OPENMW_MWSOUND_MOVIEAUDIOFACTORY_H +#define OPENMW_MWSOUND_MOVIEAUDIOFACTORY_H + +#include + +namespace MWSound +{ + + class MovieAudioFactory : public Video::MovieAudioFactory + { + virtual boost::shared_ptr createDecoder(Video::VideoState* videoState); + }; + +} + +#endif diff --git a/apps/openmw/mwsound/openal_output.cpp b/apps/openmw/mwsound/openal_output.cpp index b245b9241..bc9478945 100644 --- a/apps/openmw/mwsound/openal_output.cpp +++ b/apps/openmw/mwsound/openal_output.cpp @@ -11,11 +11,16 @@ #include "sound_decoder.hpp" #include "sound.hpp" #include "soundmanagerimp.hpp" +#include "loudness.hpp" #ifndef ALC_ALL_DEVICES_SPECIFIER #define ALC_ALL_DEVICES_SPECIFIER 0x1013 #endif +namespace +{ + const int loudnessFPS = 20; // loudness values per second of audio +} namespace MWSound { @@ -691,7 +696,7 @@ void OpenAL_Output::init(const std::string &devname) fail(std::string("Failed to setup context: ")+alcGetString(mDevice, alcGetError(mDevice))); } - alDistanceModel(AL_LINEAR_DISTANCE_CLAMPED); + alDistanceModel(AL_INVERSE_DISTANCE_CLAMPED); throwALerror(); ALCint maxmono=0, maxstereo=0; @@ -734,7 +739,7 @@ void OpenAL_Output::deinit() mUnusedBuffers.clear(); while(!mBufferCache.empty()) { - alDeleteBuffers(1, &mBufferCache.begin()->second); + alDeleteBuffers(1, &mBufferCache.begin()->second.mALBuffer); mBufferCache.erase(mBufferCache.begin()); } @@ -750,14 +755,14 @@ void OpenAL_Output::deinit() } -ALuint OpenAL_Output::getBuffer(const std::string &fname) +const CachedSound& OpenAL_Output::getBuffer(const std::string &fname) { ALuint buf = 0; NameMap::iterator iditer = mBufferCache.find(fname); if(iditer != mBufferCache.end()) { - buf = iditer->second; + buf = iditer->second.mALBuffer; if(mBufferRefs[buf]++ == 0) { IDDq::iterator iter = std::find(mUnusedBuffers.begin(), @@ -766,7 +771,7 @@ ALuint OpenAL_Output::getBuffer(const std::string &fname) mUnusedBuffers.erase(iter); } - return buf; + return iditer->second; } throwALerror(); @@ -795,12 +800,16 @@ ALuint OpenAL_Output::getBuffer(const std::string &fname) decoder->readAll(data); decoder->close(); + CachedSound cached; + analyzeLoudness(data, srate, chans, type, cached.mLoudnessVector, loudnessFPS); + alGenBuffers(1, &buf); throwALerror(); alBufferData(buf, format, &data[0], data.size(), srate); - mBufferCache[fname] = buf; mBufferRefs[buf] = 1; + cached.mALBuffer = buf; + mBufferCache[fname] = cached; ALint bufsize = 0; alGetBufferi(buf, AL_SIZE, &bufsize); @@ -821,7 +830,7 @@ ALuint OpenAL_Output::getBuffer(const std::string &fname) NameMap::iterator nameiter = mBufferCache.begin(); while(nameiter != mBufferCache.end()) { - if(nameiter->second == oldbuf) + if(nameiter->second.mALBuffer == oldbuf) mBufferCache.erase(nameiter++); else ++nameiter; @@ -832,7 +841,8 @@ ALuint OpenAL_Output::getBuffer(const std::string &fname) alDeleteBuffers(1, &oldbuf); mBufferCacheMemSize -= bufsize; } - return buf; + + return mBufferCache[fname]; } void OpenAL_Output::bufferFinished(ALuint buf) @@ -856,7 +866,7 @@ MWBase::SoundPtr OpenAL_Output::playSound(const std::string &fname, float vol, f try { - buf = getBuffer(fname); + buf = getBuffer(fname).mALBuffer; sound.reset(new OpenAL_Sound(*this, src, buf, Ogre::Vector3(0.0f), vol, basevol, pitch, 1.0f, 1000.0f, flags)); } catch(std::exception&) @@ -883,7 +893,7 @@ MWBase::SoundPtr OpenAL_Output::playSound(const std::string &fname, float vol, f } MWBase::SoundPtr OpenAL_Output::playSound3D(const std::string &fname, const Ogre::Vector3 &pos, float vol, float basevol, float pitch, - float min, float max, int flags, float offset) + float min, float max, int flags, float offset, bool extractLoudness) { boost::shared_ptr sound; ALuint src=0, buf=0; @@ -895,8 +905,12 @@ MWBase::SoundPtr OpenAL_Output::playSound3D(const std::string &fname, const Ogre try { - buf = getBuffer(fname); + const CachedSound& cached = getBuffer(fname); + buf = cached.mALBuffer; + sound.reset(new OpenAL_Sound3D(*this, src, buf, pos, vol, basevol, pitch, min, max, flags)); + if (extractLoudness) + sound->setLoudnessVector(cached.mLoudnessVector, loudnessFPS); } catch(std::exception&) { diff --git a/apps/openmw/mwsound/openal_output.hpp b/apps/openmw/mwsound/openal_output.hpp index 31edf7359..be12bfbec 100644 --- a/apps/openmw/mwsound/openal_output.hpp +++ b/apps/openmw/mwsound/openal_output.hpp @@ -16,6 +16,12 @@ namespace MWSound class SoundManager; class Sound; + struct CachedSound + { + ALuint mALBuffer; + std::vector mLoudnessVector; + }; + class OpenAL_Output : public Sound_Output { ALCdevice *mDevice; @@ -25,7 +31,7 @@ namespace MWSound IDDq mFreeSources; IDDq mUnusedBuffers; - typedef std::map NameMap; + typedef std::map NameMap; NameMap mBufferCache; typedef std::map IDRefMap; @@ -36,7 +42,7 @@ namespace MWSound typedef std::vector SoundVec; SoundVec mActiveSounds; - ALuint getBuffer(const std::string &fname); + const CachedSound& getBuffer(const std::string &fname); void bufferFinished(ALuint buffer); Environment mLastEnvironment; @@ -49,7 +55,7 @@ namespace MWSound virtual MWBase::SoundPtr playSound(const std::string &fname, float vol, float basevol, float pitch, int flags, float offset); /// @param offset Value from [0,1] meaning from which fraction the sound the playback starts. virtual MWBase::SoundPtr playSound3D(const std::string &fname, const Ogre::Vector3 &pos, - float vol, float basevol, float pitch, float min, float max, int flags, float offset); + float vol, float basevol, float pitch, float min, float max, int flags, float offset, bool extractLoudness=false); virtual MWBase::SoundPtr streamSound(DecoderPtr decoder, float volume, float pitch, int flags); virtual void updateListener(const Ogre::Vector3 &pos, const Ogre::Vector3 &atdir, const Ogre::Vector3 &updir, Environment env); diff --git a/apps/openmw/mwsound/sound.cpp b/apps/openmw/mwsound/sound.cpp new file mode 100644 index 000000000..b3105a82c --- /dev/null +++ b/apps/openmw/mwsound/sound.cpp @@ -0,0 +1,23 @@ +#include "sound.hpp" + +namespace MWSound +{ + + float Sound::getCurrentLoudness() + { + if (mLoudnessVector.empty()) + return 0.f; + int index = getTimeOffset() * mLoudnessFPS; + + index = std::max(0, std::min(index, int(mLoudnessVector.size()-1))); + + return mLoudnessVector[index]; + } + + void Sound::setLoudnessVector(const std::vector &loudnessVector, float loudnessFPS) + { + mLoudnessVector = loudnessVector; + mLoudnessFPS = loudnessFPS; + } + +} diff --git a/apps/openmw/mwsound/sound.hpp b/apps/openmw/mwsound/sound.hpp index 670002a30..1b5c00196 100644 --- a/apps/openmw/mwsound/sound.hpp +++ b/apps/openmw/mwsound/sound.hpp @@ -24,6 +24,9 @@ namespace MWSound int mFlags; float mFadeOutTime; + std::vector mLoudnessVector; + float mLoudnessFPS; + public: virtual void stop() = 0; virtual bool isPlaying() = 0; @@ -31,6 +34,12 @@ namespace MWSound void setPosition(const Ogre::Vector3 &pos) { mPos = pos; } void setVolume(float volume) { mVolume = volume; } void setFadeout(float duration) { mFadeOutTime=duration; } + void setLoudnessVector(const std::vector& loudnessVector, float loudnessFPS); + + /// Get loudness at the current time position on a [0,1] scale. + /// Requires that loudnessVector was filled in by the user. + float getCurrentLoudness(); + MWBase::SoundManager::PlayType getPlayType() const { return (MWBase::SoundManager::PlayType)(mFlags&MWBase::SoundManager::Play_TypeMask); } @@ -44,6 +53,7 @@ namespace MWSound , mMaxDistance(maxdist) , mFlags(flags) , mFadeOutTime(0) + , mLoudnessFPS(20) { } virtual ~Sound() { } diff --git a/apps/openmw/mwsound/sound_output.hpp b/apps/openmw/mwsound/sound_output.hpp index 91e25db52..a9a999a5c 100644 --- a/apps/openmw/mwsound/sound_output.hpp +++ b/apps/openmw/mwsound/sound_output.hpp @@ -28,7 +28,7 @@ namespace MWSound virtual MWBase::SoundPtr playSound(const std::string &fname, float vol, float basevol, float pitch, int flags, float offset) = 0; /// @param offset Value from [0,1] meaning from which fraction the sound the playback starts. virtual MWBase::SoundPtr playSound3D(const std::string &fname, const Ogre::Vector3 &pos, - float vol, float basevol, float pitch, float min, float max, int flags, float offset) = 0; + float vol, float basevol, float pitch, float min, float max, int flags, float offset, bool extractLoudness=false) = 0; virtual MWBase::SoundPtr streamSound(DecoderPtr decoder, float volume, float pitch, int flags) = 0; virtual void updateListener(const Ogre::Vector3 &pos, const Ogre::Vector3 &atdir, const Ogre::Vector3 &updir, Environment env) = 0; diff --git a/apps/openmw/mwsound/soundmanagerimp.cpp b/apps/openmw/mwsound/soundmanagerimp.cpp index ba7b4f3ba..0bc155118 100644 --- a/apps/openmw/mwsound/soundmanagerimp.cpp +++ b/apps/openmw/mwsound/soundmanagerimp.cpp @@ -17,15 +17,10 @@ #include "openal_output.hpp" #define SOUND_OUT "OpenAL" -/* Set up the sound manager to use FFMPEG for input. - * The OPENMW_USE_x macros are set in CMakeLists.txt. -*/ -#ifdef OPENMW_USE_FFMPEG #include "ffmpeg_decoder.hpp" #ifndef SOUND_IN #define SOUND_IN "FFmpeg" #endif -#endif namespace MWSound @@ -108,24 +103,31 @@ namespace MWSound std::string SoundManager::lookup(const std::string &soundId, float &volume, float &min, float &max) { - const ESM::Sound *snd = - MWBase::Environment::get().getWorld()->getStore().get().find(soundId); + MWBase::World* world = MWBase::Environment::get().getWorld(); + const ESM::Sound *snd = world->getStore().get().find(soundId); volume *= pow(10.0, (snd->mData.mVolume/255.0*3348.0 - 3348.0) / 2000.0); if(snd->mData.mMinRange == 0 && snd->mData.mMaxRange == 0) { - min = 100.0f; - max = 2000.0f; + static const float fAudioDefaultMinDistance = world->getStore().get().find("fAudioDefaultMinDistance")->getFloat(); + static const float fAudioDefaultMaxDistance = world->getStore().get().find("fAudioDefaultMaxDistance")->getFloat(); + min = fAudioDefaultMinDistance; + max = fAudioDefaultMaxDistance; } else { - min = snd->mData.mMinRange * 20.0f; - max = snd->mData.mMaxRange * 50.0f; - min = std::max(min, 1.0f); - max = std::max(min, max); + min = snd->mData.mMinRange; + max = snd->mData.mMaxRange; } + static const float fAudioMinDistanceMult = world->getStore().get().find("fAudioMinDistanceMult")->getFloat(); + static const float fAudioMaxDistanceMult = world->getStore().get().find("fAudioMaxDistanceMult")->getFloat(); + min *= fAudioMinDistanceMult; + max *= fAudioMaxDistanceMult; + min = std::max(min, 1.0f); + max = std::max(min, max); + return "Sound/"+snd->mSound; } @@ -255,8 +257,19 @@ namespace MWSound const ESM::Position &pos = ptr.getRefData().getPosition(); const Ogre::Vector3 objpos(pos.pos); + MWBase::World* world = MWBase::Environment::get().getWorld(); + static const float fAudioMinDistanceMult = world->getStore().get().find("fAudioMinDistanceMult")->getFloat(); + static const float fAudioMaxDistanceMult = world->getStore().get().find("fAudioMaxDistanceMult")->getFloat(); + static const float fAudioVoiceDefaultMinDistance = world->getStore().get().find("fAudioVoiceDefaultMinDistance")->getFloat(); + static const float fAudioVoiceDefaultMaxDistance = world->getStore().get().find("fAudioVoiceDefaultMaxDistance")->getFloat(); + + float minDistance = fAudioVoiceDefaultMinDistance * fAudioMinDistanceMult; + float maxDistance = fAudioVoiceDefaultMaxDistance * fAudioMaxDistanceMult; + minDistance = std::max(minDistance, 1.f); + maxDistance = std::max(minDistance, maxDistance); + MWBase::SoundPtr sound = mOutput->playSound3D(filePath, objpos, 1.0f, basevol, 1.0f, - 20.0f, 1500.0f, Play_Normal|Play_TypeVoice, 0); + minDistance, maxDistance, Play_Normal|Play_TypeVoice, 0, true); mActiveSounds[sound] = std::make_pair(ptr, std::string("_say_sound")); } catch(std::exception &e) @@ -265,6 +278,21 @@ namespace MWSound } } + float SoundManager::getSaySoundLoudness(const MWWorld::Ptr &ptr) const + { + SoundMap::const_iterator snditer = mActiveSounds.begin(); + while(snditer != mActiveSounds.end()) + { + if(snditer->second.first == ptr && snditer->second.second == "_say_sound") + break; + ++snditer; + } + if (snditer == mActiveSounds.end()) + return 0.f; + + return snditer->first->getCurrentLoudness(); + } + void SoundManager::say(const std::string& filename) { if(!mOutput->isInitialized()) @@ -357,6 +385,11 @@ namespace MWSound const ESM::Position &pos = ptr.getRefData().getPosition(); const Ogre::Vector3 objpos(pos.pos); + if ((mode & Play_RemoveAtDistance) && mListenerPos.squaredDistance(objpos) > 2000*2000) + { + return MWBase::SoundPtr(); + } + sound = mOutput->playSound3D(file, objpos, volume, basevol, pitch, min, max, mode|type, offset); if((mode&Play_NoTrack)) mActiveSounds[sound] = std::make_pair(MWWorld::Ptr(), soundId); @@ -444,6 +477,7 @@ namespace MWSound while(snditer != mActiveSounds.end()) { if(snditer->second.first != MWWorld::Ptr() && + snditer->second.first.getCellRef().getRefId() != "player" && snditer->second.first.getCell() == cell) { snditer->first->stop(); @@ -621,6 +655,13 @@ namespace MWSound const ESM::Position &pos = ptr.getRefData().getPosition(); const Ogre::Vector3 objpos(pos.pos); snditer->first->setPosition(objpos); + + if ((snditer->first->mFlags & Play_RemoveAtDistance) + && mListenerPos.squaredDistance(Ogre::Vector3(ptr.getRefData().getPosition().pos)) > 2000*2000) + { + mActiveSounds.erase(snditer++); + continue; + } } //update fade out if(snditer->first->mFadeOutTime>0) @@ -682,9 +723,9 @@ namespace MWSound MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); - const ESM::Cell *cell = player.getCell()->getCell(); + const MWWorld::CellStore *cell = player.getCell(); - mListenerUnderwater = ((cell->mData.mFlags&cell->HasWater) && mListenerPos.z < cell->mWater); + mListenerUnderwater = ((cell->getCell()->mData.mFlags&ESM::Cell::HasWater) && mListenerPos.z < cell->getWaterLevel()); } void SoundManager::updatePtr(const MWWorld::Ptr &old, const MWWorld::Ptr &updated) diff --git a/apps/openmw/mwsound/soundmanagerimp.hpp b/apps/openmw/mwsound/soundmanagerimp.hpp index 380cfe255..250cb0d51 100644 --- a/apps/openmw/mwsound/soundmanagerimp.hpp +++ b/apps/openmw/mwsound/soundmanagerimp.hpp @@ -110,6 +110,11 @@ namespace MWSound virtual void stopSay(const MWWorld::Ptr &reference=MWWorld::Ptr()); ///< Stop an actor speaking + virtual float getSaySoundLoudness(const MWWorld::Ptr& reference) const; + ///< Check the currently playing say sound for this actor + /// and get an average loudness value (scale [0,1]) at the current time position. + /// If the actor is not saying anything, returns 0. + virtual MWBase::SoundPtr playTrack(const DecoderPtr& decoder, PlayType type); ///< Play a 2D audio track, using a custom decoder diff --git a/apps/openmw/mwstate/character.cpp b/apps/openmw/mwstate/character.cpp index 6f569f078..f190565da 100644 --- a/apps/openmw/mwstate/character.cpp +++ b/apps/openmw/mwstate/character.cpp @@ -14,9 +14,6 @@ #include -#include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" - bool MWState::operator< (const Slot& left, const Slot& right) { return left.mTimeStamp #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -27,6 +28,7 @@ #include "../mwworld/player.hpp" #include "../mwworld/class.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwmechanics/npcstats.hpp" @@ -117,7 +119,7 @@ void MWState::StateManager::askLoadRecent() std::string message = MWBase::Environment::get().getWindowManager()->getGameSettingString("sLoadLastSaveMsg", tag); size_t pos = message.find(tag); message.replace(pos, tag.length(), lastSave.mProfile.mDescription); - MWBase::Environment::get().getWindowManager()->messageBox(message, buttons); + MWBase::Environment::get().getWindowManager()->interactiveMessageBox(message, buttons); mAskLoadRecent = true; } } @@ -132,12 +134,12 @@ void MWState::StateManager::newGame (bool bypass) { cleanup(); - MWBase::Environment::get().getWorld()->startNewGame (bypass); - if (!bypass) MWBase::Environment::get().getWindowManager()->setNewGame (true); - else - MWBase::Environment::get().getWorld()->setGlobalInt ("chargenstate", -1); + + MWBase::Environment::get().getScriptManager()->getGlobalScripts().addStartup(); + + MWBase::Environment::get().getWorld()->startNewGame (bypass); mState = State_Running; } @@ -159,7 +161,7 @@ void MWState::StateManager::saveGame (const std::string& description, const Slot profile.mContentFiles = world.getContentFiles(); - profile.mPlayerName = player.getClass().getName (player); + profile.mPlayerName = player.get()->mBase->mName; profile.mPlayerLevel = player.getClass().getNpcStats (player).getLevel(); std::string classId = player.get()->mBase->mClass; @@ -220,7 +222,8 @@ void MWState::StateManager::saveGame (const std::string& description, const Slot writer.save (stream); Loading::Listener& listener = *MWBase::Environment::get().getWindowManager()->getLoadingScreen(); - listener.setProgressRange(recordCount); + // Using only Cells for progress information, since they typically have the largest records by far + listener.setProgressRange(MWBase::Environment::get().getWorld()->countSavedGameCells()); listener.setLabel("#{sNotifyMessage4}"); Loading::ScopedLoad load(&listener); @@ -228,7 +231,6 @@ void MWState::StateManager::saveGame (const std::string& description, const Slot writer.startRecord (ESM::REC_SAVE); slot->mProfile.save (writer); writer.endRecord (ESM::REC_SAVE); - listener.increaseProgress(); MWBase::Environment::get().getJournal()->write (writer, listener); MWBase::Environment::get().getDialogueManager()->write (writer, listener); @@ -258,7 +260,7 @@ void MWState::StateManager::saveGame (const std::string& description, const Slot std::vector buttons; buttons.push_back("#{sOk}"); - MWBase::Environment::get().getWindowManager()->messageBox(error.str(), buttons); + MWBase::Environment::get().getWindowManager()->interactiveMessageBox(error.str(), buttons); // If no file was written, clean up the slot if (slot && !boost::filesystem::exists(slot->mPath)) @@ -290,26 +292,61 @@ void MWState::StateManager::quickSave (std::string name) saveGame(name, slot); } -void MWState::StateManager::loadGame (const Character *character, const Slot *slot) +void MWState::StateManager::loadGame(const std::string& filepath) +{ + for (CharacterIterator it = mCharacterManager.begin(); it != mCharacterManager.end(); ++it) + { + const MWState::Character& character = *it; + for (MWState::Character::SlotIterator slotIt = character.begin(); slotIt != character.end(); ++slotIt) + { + const MWState::Slot& slot = *slotIt; + if (slot.mPath == boost::filesystem::path(filepath)) + { + loadGame(&character, slot.mPath.string()); + return; + } + } + } + + // have to peek into the save file to get the player name + ESM::ESMReader reader; + reader.open (filepath); + if (reader.getFormat()>ESM::Header::CurrentFormat) + return; // format is too new -> ignore + if (reader.getRecName()!=ESM::REC_SAVE) + return; // invalid save file -> ignore + reader.getRecHeader(); + ESM::SavedGame profile; + profile.load (reader); + reader.close(); + + MWState::Character* character = mCharacterManager.getCurrentCharacter(true, profile.mPlayerName); + loadGame(character, filepath); + mTimePlayed = profile.mTimePlayed; +} + +void MWState::StateManager::loadGame (const Character *character, const std::string& filepath) { try { cleanup(); - mTimePlayed = slot->mProfile.mTimePlayed; - ESM::ESMReader reader; - reader.open (slot->mPath.string()); + reader.open (filepath); std::map contentFileMap = buildContentFileIndexMap (reader); Loading::Listener& listener = *MWBase::Environment::get().getWindowManager()->getLoadingScreen(); - listener.setProgressRange(reader.getRecordCount()); + listener.setProgressRange(100); listener.setLabel("#{sLoadingMessage14}"); Loading::ScopedLoad load(&listener); + bool firstPersonCam = false; + + size_t total = reader.getFileSize(); + int currentPercent = 0; while (reader.hasMoreRecs()) { ESM::NAME n = reader.getRecName(); @@ -318,12 +355,21 @@ void MWState::StateManager::loadGame (const Character *character, const Slot *sl switch (n.val) { case ESM::REC_SAVE: - - // don't need to read that here - reader.skipRecord(); + { + ESM::SavedGame profile; + profile.load(reader); + if (!verifyProfile(profile)) + { + cleanup (true); + MWBase::Environment::get().getWindowManager()->pushGuiMode (MWGui::GM_MainMenu); + return; + } + mTimePlayed = profile.mTimePlayed; + } break; case ESM::REC_JOUR: + case ESM::REC_JOUR_LEGACY: case ESM::REC_QUES: MWBase::Environment::get().getJournal()->readRecord (reader, n.val); @@ -351,8 +397,14 @@ void MWState::StateManager::loadGame (const Character *character, const Slot *sl case ESM::REC_ACTC: case ESM::REC_PROJ: case ESM::REC_MPRJ: + case ESM::REC_ENAB: + case ESM::REC_LEVC: + case ESM::REC_LEVI: + MWBase::Environment::get().getWorld()->readRecord(reader, n.val, contentFileMap); + break; - MWBase::Environment::get().getWorld()->readRecord (reader, n.val, contentFileMap); + case ESM::REC_CAM_: + reader.getHNT(firstPersonCam, "FIRS"); break; case ESM::REC_GSCR: @@ -363,11 +415,13 @@ void MWState::StateManager::loadGame (const Character *character, const Slot *sl case ESM::REC_GMAP: case ESM::REC_KEYS: case ESM::REC_ASPL: + case ESM::REC_MARK: MWBase::Environment::get().getWindowManager()->readRecord(reader, n.val); break; case ESM::REC_DCOU: + case ESM::REC_STLN: MWBase::Environment::get().getMechanicsManager()->readRecord(reader, n.val); break; @@ -375,10 +429,15 @@ void MWState::StateManager::loadGame (const Character *character, const Slot *sl default: // ignore invalid records - /// \todo log error + std::cerr << "Ignoring unknown record: " << n.toString() << std::endl; reader.skipRecord(); } - listener.increaseProgress(); + int progressPercent = static_cast(float(reader.getFileOffset())/total*100); + if (progressPercent > currentPercent) + { + listener.increaseProgress(progressPercent-currentPercent); + currentPercent = progressPercent; + } } mCharacterManager.setCurrentCharacter(character); @@ -386,7 +445,7 @@ void MWState::StateManager::loadGame (const Character *character, const Slot *sl mState = State_Running; Settings::Manager::setString ("character", "Saves", - slot->mPath.parent_path().filename().string()); + character->getPath().filename().string()); MWBase::Environment::get().getWindowManager()->setNewGame(false); MWBase::Environment::get().getWorld()->setupPlayer(); @@ -394,6 +453,9 @@ void MWState::StateManager::loadGame (const Character *character, const Slot *sl MWBase::Environment::get().getWindowManager()->updatePlayer(); MWBase::Environment::get().getMechanicsManager()->playerLoaded(); + if (firstPersonCam != MWBase::Environment::get().getWorld()->isFirstPerson()) + MWBase::Environment::get().getWorld()->togglePOV(); + MWWorld::Ptr ptr = MWBase::Environment::get().getWorld()->getPlayerPtr(); ESM::CellId cellId = ptr.getCell()->getCell()->getCellId(); @@ -401,6 +463,10 @@ void MWState::StateManager::loadGame (const Character *character, const Slot *sl // Use detectWorldSpaceChange=false, otherwise some of the data we just loaded would be cleared again MWBase::Environment::get().getWorld()->changeToCell (cellId, ptr.getRefData().getPosition(), false); + // Vanilla MW will restart startup scripts when a save game is loaded. This is unintuitive, + // but some mods may be using it as a reload detector. + MWBase::Environment::get().getScriptManager()->getGlobalScripts().addStartup(); + // Do not trigger erroneous cellChanged events MWBase::Environment::get().getWorld()->markCellAsUnchanged(); } @@ -416,15 +482,18 @@ void MWState::StateManager::loadGame (const Character *character, const Slot *sl std::vector buttons; buttons.push_back("#{sOk}"); - MWBase::Environment::get().getWindowManager()->messageBox(error.str(), buttons); + MWBase::Environment::get().getWindowManager()->interactiveMessageBox(error.str(), buttons); } } void MWState::StateManager::quickLoad() { if (Character* mCurrentCharacter = getCurrentCharacter (false)) - if (const MWState::Slot* slot = &*mCurrentCharacter->begin()) //Get newest save - loadGame (mCurrentCharacter, slot); + { + if (mCurrentCharacter->begin() == mCurrentCharacter->end()) + return; + loadGame (mCurrentCharacter, mCurrentCharacter->begin()->mPath.string()); //Get newest save + } } void MWState::StateManager::deleteGame(const MWState::Character *character, const MWState::Slot *slot) @@ -435,7 +504,7 @@ void MWState::StateManager::deleteGame(const MWState::Character *character, cons MWState::Character *MWState::StateManager::getCurrentCharacter (bool create) { MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); - std::string name = player.getClass().getName(player); + std::string name = player.get()->mBase->mName; return mCharacterManager.getCurrentCharacter (create, name); } @@ -465,7 +534,7 @@ void MWState::StateManager::update (float duration) //Load last saved game for current character MWState::Slot lastSave = *curCharacter->begin(); - loadGame(curCharacter, &lastSave); + loadGame(curCharacter, lastSave.mPath.string()); } else if(iButton==1) { @@ -474,3 +543,30 @@ void MWState::StateManager::update (float duration) } } } + +bool MWState::StateManager::verifyProfile(const ESM::SavedGame& profile) const +{ + const std::vector& selectedContentFiles = MWBase::Environment::get().getWorld()->getContentFiles(); + bool notFound = false; + for (std::vector::const_iterator it = profile.mContentFiles.begin(); + it != profile.mContentFiles.end(); ++it) + { + if (std::find(selectedContentFiles.begin(), selectedContentFiles.end(), *it) + == selectedContentFiles.end()) + { + std::cerr << "Savegame dependency " << *it << " is missing." << std::endl; + notFound = true; + } + } + if (notFound) + { + std::vector buttons; + buttons.push_back("#{sYes}"); + buttons.push_back("#{sNo}"); + MWBase::Environment::get().getWindowManager()->interactiveMessageBox("#{sMissingMastersMsg}", buttons, true); + int selectedButton = MWBase::Environment::get().getWindowManager()->readPressedButton(); + if (selectedButton == 1 || selectedButton == -1) + return false; + } + return true; +} diff --git a/apps/openmw/mwstate/statemanagerimp.hpp b/apps/openmw/mwstate/statemanagerimp.hpp index 40c36deb5..37f38f8df 100644 --- a/apps/openmw/mwstate/statemanagerimp.hpp +++ b/apps/openmw/mwstate/statemanagerimp.hpp @@ -23,6 +23,8 @@ namespace MWState void cleanup (bool force = false); + bool verifyProfile (const ESM::SavedGame& profile) const; + std::map buildContentFileIndexMap (const ESM::ESMReader& reader) const; public: @@ -61,10 +63,13 @@ namespace MWState /** Used for quickload **/ virtual void quickLoad(); - virtual void loadGame (const Character *character, const Slot *slot); - ///< Load a saved game file from \a slot. - /// - /// \note \a slot must belong to \a character. + virtual void loadGame (const std::string& filepath); + ///< Load a saved game directly from the given file path. This will search the CharacterManager + /// for a Character containing this save file, and set this Character current if one was found. + /// Otherwise, a new Character will be created. + + virtual void loadGame (const Character *character, const std::string &filepath); + ///< Load a saved game file belonging to the given character. virtual Character *getCurrentCharacter (bool create = true); ///< \param create Create a new character, if there is no current character. diff --git a/apps/openmw/mwworld/actionequip.cpp b/apps/openmw/mwworld/actionequip.cpp index 50da1e5e5..87a4c6308 100644 --- a/apps/openmw/mwworld/actionequip.cpp +++ b/apps/openmw/mwworld/actionequip.cpp @@ -31,11 +31,7 @@ namespace MWWorld { case 0: return; - case 2: - invStore.unequipSlot(MWWorld::InventoryStore::Slot_CarriedLeft, actor); - break; - case 3: - invStore.unequipSlot(MWWorld::InventoryStore::Slot_CarriedRight, actor); + default: break; } diff --git a/apps/openmw/mwworld/actiontake.cpp b/apps/openmw/mwworld/actiontake.cpp index 548a94981..269d941dc 100644 --- a/apps/openmw/mwworld/actiontake.cpp +++ b/apps/openmw/mwworld/actiontake.cpp @@ -16,7 +16,7 @@ namespace MWWorld void ActionTake::executeImp (const Ptr& actor) { MWBase::Environment::get().getMechanicsManager()->itemTaken( - actor, getTarget(), getTarget().getRefData().getCount()); + actor, getTarget(), MWWorld::Ptr(), getTarget().getRefData().getCount()); actor.getClass().getContainerStore (actor).add (getTarget(), getTarget().getRefData().getCount(), actor); MWBase::Environment::get().getWorld()->deleteObject (getTarget()); } diff --git a/apps/openmw/mwworld/actionteleport.cpp b/apps/openmw/mwworld/actionteleport.cpp index 4378e179d..8bbb08008 100644 --- a/apps/openmw/mwworld/actionteleport.cpp +++ b/apps/openmw/mwworld/actionteleport.cpp @@ -1,4 +1,3 @@ - #include "actionteleport.hpp" #include "../mwbase/environment.hpp" @@ -6,6 +5,23 @@ #include "../mwbase/mechanicsmanager.hpp" #include "player.hpp" +namespace +{ + + void getFollowers (const MWWorld::Ptr& actor, std::set& out) + { + std::list followers = MWBase::Environment::get().getMechanicsManager()->getActorsFollowing(actor); + for(std::list::iterator it = followers.begin();it != followers.end();++it) + { + if (out.insert(*it).second) + { + getFollowers(*it, out); + } + } + } + +} + namespace MWWorld { ActionTeleport::ActionTeleport (const std::string& cellName, @@ -16,15 +32,24 @@ namespace MWWorld void ActionTeleport::executeImp (const Ptr& actor) { - MWBase::World* world = MWBase::Environment::get().getWorld(); - //find any NPC that is following the actor and teleport him too - std::list followers = MWBase::Environment::get().getMechanicsManager()->getActorsFollowing(actor); - for(std::list::iterator it = followers.begin();it != followers.end();++it) + std::set followers; + getFollowers(actor, followers); + for(std::set::iterator it = followers.begin();it != followers.end();++it) { - executeImp(*it); + MWWorld::Ptr follower = *it; + if (Ogre::Vector3(follower.getRefData().getPosition().pos).squaredDistance( + Ogre::Vector3( actor.getRefData().getPosition().pos)) + <= 800*800) + teleport(*it); } + teleport(actor); + } + + void ActionTeleport::teleport(const Ptr &actor) + { + MWBase::World* world = MWBase::Environment::get().getWorld(); if(actor == world->getPlayerPtr()) { world->getPlayer().setTeleported(true); diff --git a/apps/openmw/mwworld/actionteleport.hpp b/apps/openmw/mwworld/actionteleport.hpp index a13cb61b2..9ca664de8 100644 --- a/apps/openmw/mwworld/actionteleport.hpp +++ b/apps/openmw/mwworld/actionteleport.hpp @@ -14,12 +14,16 @@ namespace MWWorld std::string mCellName; ESM::Position mPosition; + /// Teleports this actor and also teleports anyone following that actor. virtual void executeImp (const Ptr& actor); + /// Teleports only the given actor (internal use). + void teleport(const Ptr &actor); + public: ActionTeleport (const std::string& cellName, const ESM::Position& position); - ///< If cellName is empty, an exterior cell is asumed. + ///< If cellName is empty, an exterior cell is assumed. }; } diff --git a/apps/openmw/mwworld/actiontrap.cpp b/apps/openmw/mwworld/actiontrap.cpp index 1472afc08..d153b7e61 100644 --- a/apps/openmw/mwworld/actiontrap.cpp +++ b/apps/openmw/mwworld/actiontrap.cpp @@ -1,16 +1,39 @@ #include "actiontrap.hpp" #include "../mwmechanics/spellcasting.hpp" +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" namespace MWWorld { void ActionTrap::executeImp(const Ptr &actor) { - MWMechanics::CastSpell cast(mTrapSource, actor); - cast.mHitPosition = Ogre::Vector3(actor.getRefData().getPosition().pos); - cast.cast(mSpellId); + Ogre::Vector3 actorPosition(actor.getRefData().getPosition().pos); + Ogre::Vector3 trapPosition(mTrapSource.getRefData().getPosition().pos); + float activationDistance = MWBase::Environment::get().getWorld()->getMaxActivationDistance(); + // GUI calcs if object in activation distance include object and player geometry + const float fudgeFactor = 1.25f; + + // Hack: if actor is beyond activation range, then assume actor is using telekinesis + // to open door/container. + // Note, can't just detonate the trap at the trapped object's location and use the blast + // radius, because for most trap spells this is 1 foot, much less than the activation distance. + if (trapPosition.distance(actorPosition) < (activationDistance * fudgeFactor)) + { + // assume actor touched trap + MWMechanics::CastSpell cast(mTrapSource, actor); + cast.mHitPosition = actorPosition; + cast.cast(mSpellId); + } + else + { + // assume telekinesis used + MWMechanics::CastSpell cast(mTrapSource, mTrapSource); + cast.mHitPosition = trapPosition; + cast.cast(mSpellId); + } mTrapSource.getCellRef().setTrap(""); } diff --git a/apps/openmw/mwworld/cellref.cpp b/apps/openmw/mwworld/cellref.cpp index cdf08e6ed..0d81e0636 100644 --- a/apps/openmw/mwworld/cellref.cpp +++ b/apps/openmw/mwworld/cellref.cpp @@ -5,11 +5,21 @@ namespace MWWorld { - ESM::RefNum CellRef::getRefNum() const + const ESM::RefNum& CellRef::getRefNum() const { return mCellRef.mRefNum; } + bool CellRef::hasContentFile() const + { + return mCellRef.mRefNum.hasContentFile(); + } + + void CellRef::unsetRefNum() + { + mCellRef.mRefNum.unset(); + } + std::string CellRef::getRefId() const { return mCellRef.mRefID; @@ -71,15 +81,29 @@ namespace MWWorld int CellRef::getCharge() const { - return mCellRef.mCharge; + return mCellRef.mChargeInt; } void CellRef::setCharge(int charge) { - if (charge != mCellRef.mCharge) + if (charge != mCellRef.mChargeInt) { mChanged = true; - mCellRef.mCharge = charge; + mCellRef.mChargeInt = charge; + } + } + + float CellRef::getChargeFloat() const + { + return mCellRef.mChargeFloat; + } + + void CellRef::setChargeFloat(float charge) + { + if (charge != mCellRef.mChargeFloat) + { + mChanged = true; + mCellRef.mChargeFloat = charge; } } @@ -93,6 +117,24 @@ namespace MWWorld return mCellRef.mGlobalVariable; } + void CellRef::resetGlobalVariable() + { + if (!mCellRef.mGlobalVariable.empty()) + { + mChanged = true; + mCellRef.mGlobalVariable.erase(); + } + } + + void CellRef::setFactionRank(int factionRank) + { + if (factionRank != mCellRef.mFactionRank) + { + mChanged = true; + mCellRef.mFactionRank = factionRank; + } + } + int CellRef::getFactionRank() const { return mCellRef.mFactionRank; diff --git a/apps/openmw/mwworld/cellref.hpp b/apps/openmw/mwworld/cellref.hpp index 577655739..b8e85f286 100644 --- a/apps/openmw/mwworld/cellref.hpp +++ b/apps/openmw/mwworld/cellref.hpp @@ -23,7 +23,13 @@ namespace MWWorld } // Note: Currently unused for items in containers - ESM::RefNum getRefNum() const; + const ESM::RefNum& getRefNum() const; + + // Set RefNum to its default state. + void unsetRefNum(); + + /// Does the RefNum have a content file? + bool hasContentFile() const; // Id of object being referenced std::string getRefId() const; @@ -54,8 +60,11 @@ namespace MWWorld // For weapon or armor, this is the remaining item health. // For tools (lockpicks, probes, repair hammer) it is the remaining uses. + // If this returns int(-1) it means full health. int getCharge() const; + float getChargeFloat() const; // Implemented as union with int charge void setCharge(int charge); + void setChargeFloat(float charge); // The NPC that owns this object (and will get angry if you steal it) std::string getOwner() const; @@ -66,6 +75,8 @@ namespace MWWorld // Used by bed rent scripts to allow the player to use the bed for the duration of the rent. std::string getGlobalVariable() const; + void resetGlobalVariable(); + // ID of creature trapped in this soul gem std::string getSoul() const; void setSoul(const std::string& soul); @@ -76,6 +87,7 @@ namespace MWWorld void setFaction (const std::string& faction); // PC faction rank required to use the item. Sometimes is -1, which means "any rank". + void setFactionRank(int factionRank); int getFactionRank() const; // Lock level for doors and containers diff --git a/apps/openmw/mwworld/cellreflist.hpp b/apps/openmw/mwworld/cellreflist.hpp index 9c3370f08..2c5e01aaa 100644 --- a/apps/openmw/mwworld/cellreflist.hpp +++ b/apps/openmw/mwworld/cellreflist.hpp @@ -27,7 +27,9 @@ namespace MWWorld LiveRef *find (const std::string& name) { for (typename List::iterator iter (mList.begin()); iter!=mList.end(); ++iter) - if (iter->mData.getCount() > 0 && iter->mRef.getRefId() == name) + if (!iter->mData.isDeletedByContentFile() + && (iter->mRef.hasContentFile() || iter->mData.getCount() > 0) + && iter->mRef.getRefId() == name) return &*iter; return 0; @@ -42,7 +44,7 @@ namespace MWWorld LiveCellRef *searchViaHandle (const std::string& handle) { for (typename List::iterator iter (mList.begin()); iter!=mList.end(); ++iter) - if (iter->mData.getCount()>0 && iter->mData.getBaseNode() && + if (iter->mData.getBaseNode() && iter->mData.getHandle()==handle) return &*iter; diff --git a/apps/openmw/mwworld/cells.cpp b/apps/openmw/mwworld/cells.cpp index ef3d299a9..2aa817fa5 100644 --- a/apps/openmw/mwworld/cells.cpp +++ b/apps/openmw/mwworld/cells.cpp @@ -241,26 +241,30 @@ MWWorld::Ptr MWWorld::Cells::getPtr (const std::string& name) void MWWorld::Cells::getExteriorPtrs(const std::string &name, std::vector &out) { - for (std::map, CellStore>::iterator iter = mExteriors.begin(); - iter!=mExteriors.end(); ++iter) + const MWWorld::Store &cells = mStore.get(); + for (MWWorld::Store::iterator iter = cells.extBegin(); iter != cells.extEnd(); ++iter) { - Ptr ptr = getPtrAndCache (name, iter->second); + CellStore *cellStore = getCellStore (&(*iter)); + + Ptr ptr = getPtrAndCache (name, *cellStore); + if (!ptr.isEmpty()) out.push_back(ptr); } - } void MWWorld::Cells::getInteriorPtrs(const std::string &name, std::vector &out) { - for (std::map::iterator iter = mInteriors.begin(); - iter!=mInteriors.end(); ++iter) + const MWWorld::Store &cells = mStore.get(); + for (MWWorld::Store::iterator iter = cells.intBegin(); iter != cells.intEnd(); ++iter) { - Ptr ptr = getPtrAndCache (name, iter->second); + CellStore *cellStore = getCellStore (&(*iter)); + + Ptr ptr = getPtrAndCache (name, *cellStore); + if (!ptr.isEmpty()) out.push_back(ptr); } - } int MWWorld::Cells::countSavedGameRecords() const @@ -287,7 +291,7 @@ void MWWorld::Cells::write (ESM::ESMWriter& writer, Loading::Listener& progress) if (iter->second.hasState()) { writeCell (writer, iter->second); - progress.increaseProgress(); // Assumes that each cell writes one record + progress.increaseProgress(); } for (std::map::iterator iter (mInteriors.begin()); @@ -295,11 +299,11 @@ void MWWorld::Cells::write (ESM::ESMWriter& writer, Loading::Listener& progress) if (iter->second.hasState()) { writeCell (writer, iter->second); - progress.increaseProgress(); // Assumes that each cell writes one record + progress.increaseProgress(); } } -bool MWWorld::Cells::readRecord (ESM::ESMReader& reader, int32_t type, +bool MWWorld::Cells::readRecord (ESM::ESMReader& reader, uint32_t type, const std::map& contentFileMap) { if (type==ESM::REC_CSTA) diff --git a/apps/openmw/mwworld/cells.hpp b/apps/openmw/mwworld/cells.hpp index a9c17fa93..0b7d9444f 100644 --- a/apps/openmw/mwworld/cells.hpp +++ b/apps/openmw/mwworld/cells.hpp @@ -76,7 +76,7 @@ namespace MWWorld void write (ESM::ESMWriter& writer, Loading::Listener& progress) const; - bool readRecord (ESM::ESMReader& reader, int32_t type, + bool readRecord (ESM::ESMReader& reader, uint32_t type, const std::map& contentFileMap); }; } diff --git a/apps/openmw/mwworld/cellstore.cpp b/apps/openmw/mwworld/cellstore.cpp index ce8d77758..7da7c187d 100644 --- a/apps/openmw/mwworld/cellstore.cpp +++ b/apps/openmw/mwworld/cellstore.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #include #include #include @@ -72,12 +71,12 @@ namespace iter (collection.mList.begin()); iter!=collection.mList.end(); ++iter) { - if (!iter->mData.hasChanged() && !iter->mRef.hasChanged() && iter->mRef.getRefNum().mContentFile != -1) + if (!iter->mData.hasChanged() && !iter->mRef.hasChanged() && iter->mRef.hasContentFile()) { // Reference that came from a content file and has not been changed -> ignore continue; } - if (iter->mData.getCount()==0 && iter->mRef.getRefNum().mContentFile==-1) + if (iter->mData.getCount()==0 && !iter->mRef.hasContentFile()) { // Deleted reference that did not come from a content file -> ignore continue; @@ -86,7 +85,9 @@ namespace RecordType state; iter->save (state); + // recordId currently unused writer.writeHNT ("OBJE", collection.mList.front().mBase->sRecordId); + state.save (writer); } } @@ -94,15 +95,16 @@ namespace template void readReferenceCollection (ESM::ESMReader& reader, - MWWorld::CellRefList& collection, const std::map& contentFileMap) + MWWorld::CellRefList& collection, const ESM::CellRef& cref, const std::map& contentFileMap) { const MWWorld::ESMStore& esmStore = MWBase::Environment::get().getWorld()->getStore(); RecordType state; - state.load (reader); + state.mRef = cref; + state.load(reader); // If the reference came from a content file, make sure this content file is loaded - if (state.mRef.mRefNum.mContentFile != -1) + if (state.mRef.mRefNum.hasContentFile()) { std::map::const_iterator iter = contentFileMap.find (state.mRef.mRefNum.mContentFile); @@ -121,7 +123,7 @@ namespace if (!record) return; - if (state.mRef.mRefNum.mContentFile != -1) + if (state.mRef.mRefNum.hasContentFile()) { for (typename MWWorld::CellRefList::List::iterator iter (collection.mList.begin()); iter!=collection.mList.end(); ++iter) @@ -156,7 +158,7 @@ namespace MWWorld LiveRef liveCellRef (ref, ptr); if (deleted) - liveCellRef.mData.setCount (0); + liveCellRef.mData.setDeleted(true); if (iter != mList.end()) *iter = liveCellRef; @@ -405,15 +407,13 @@ namespace MWWorld if (mState==State_Preloaded) mIds.clear(); - std::cout << "loading cell " << mCell->getDescription() << std::endl; - loadRefs (store, esm); mState = State_Loaded; // TODO: the pathgrid graph only needs to be loaded for active cells, so move this somewhere else. // In a simple test, loading the graph for all cells in MW + expansions took 200 ms - mPathgridGraph.load(mCell); + mPathgridGraph.load(this); } } @@ -464,7 +464,7 @@ namespace MWWorld // List moved references, from separately tracked list. for (ESM::CellRefTracker::const_iterator it = mCell->mLeasedRefs.begin(); it != mCell->mLeasedRefs.end(); ++it) { - ESM::CellRef &ref = const_cast(*it); + const ESM::CellRef &ref = *it; mIds.push_back(Misc::StringUtils::lowerCase(ref.mRefID)); } @@ -487,7 +487,7 @@ namespace MWWorld mCell->restore (esm[index], i); ESM::CellRef ref; - ref.mRefNum.mContentFile = -1; + ref.mRefNum.mContentFile = ESM::RefNum::RefNum_NoContentFile; // Get each reference in turn bool deleted = false; @@ -624,7 +624,7 @@ namespace MWWorld writeReferenceCollection (writer, mIngreds); writeReferenceCollection (writer, mCreatureLists); writeReferenceCollection (writer, mItemLists); - writeReferenceCollection (writer, mLights); + writeReferenceCollection (writer, mLights); writeReferenceCollection (writer, mLockpicks); writeReferenceCollection (writer, mMiscItems); writeReferenceCollection (writer, mNpcs); @@ -641,109 +641,121 @@ namespace MWWorld while (reader.isNextSub ("OBJE")) { - unsigned int id = 0; - reader.getHT (id); + unsigned int unused; + reader.getHT (unused); + + // load the RefID first so we know what type of object it is + ESM::CellRef cref; + cref.loadId(reader, true); + + int type = MWBase::Environment::get().getWorld()->getStore().find(cref.mRefID); + if (type == 0) + { + std::cerr << "Dropping reference to '" << cref.mRefID << "' (object no longer exists)" << std::endl; + reader.skipHSubUntil("OBJE"); + continue; + } - switch (id) + switch (type) { case ESM::REC_ACTI: - readReferenceCollection (reader, mActivators, contentFileMap); + readReferenceCollection (reader, mActivators, cref, contentFileMap); break; case ESM::REC_ALCH: - readReferenceCollection (reader, mPotions, contentFileMap); + readReferenceCollection (reader, mPotions, cref, contentFileMap); break; case ESM::REC_APPA: - readReferenceCollection (reader, mAppas, contentFileMap); + readReferenceCollection (reader, mAppas, cref, contentFileMap); break; case ESM::REC_ARMO: - readReferenceCollection (reader, mArmors, contentFileMap); + readReferenceCollection (reader, mArmors, cref, contentFileMap); break; case ESM::REC_BOOK: - readReferenceCollection (reader, mBooks, contentFileMap); + readReferenceCollection (reader, mBooks, cref, contentFileMap); break; case ESM::REC_CLOT: - readReferenceCollection (reader, mClothes, contentFileMap); + readReferenceCollection (reader, mClothes, cref, contentFileMap); break; case ESM::REC_CONT: - readReferenceCollection (reader, mContainers, contentFileMap); + readReferenceCollection (reader, mContainers, cref, contentFileMap); break; case ESM::REC_CREA: - readReferenceCollection (reader, mCreatures, contentFileMap); + readReferenceCollection (reader, mCreatures, cref, contentFileMap); break; case ESM::REC_DOOR: - readReferenceCollection (reader, mDoors, contentFileMap); + readReferenceCollection (reader, mDoors, cref, contentFileMap); break; case ESM::REC_INGR: - readReferenceCollection (reader, mIngreds, contentFileMap); + readReferenceCollection (reader, mIngreds, cref, contentFileMap); break; case ESM::REC_LEVC: - readReferenceCollection (reader, mCreatureLists, contentFileMap); + readReferenceCollection (reader, mCreatureLists, cref, contentFileMap); break; case ESM::REC_LEVI: - readReferenceCollection (reader, mItemLists, contentFileMap); + readReferenceCollection (reader, mItemLists, cref, contentFileMap); break; case ESM::REC_LIGH: - readReferenceCollection (reader, mLights, contentFileMap); + readReferenceCollection (reader, mLights, cref, contentFileMap); break; case ESM::REC_LOCK: - readReferenceCollection (reader, mLockpicks, contentFileMap); + readReferenceCollection (reader, mLockpicks, cref, contentFileMap); break; case ESM::REC_MISC: - readReferenceCollection (reader, mMiscItems, contentFileMap); + readReferenceCollection (reader, mMiscItems, cref, contentFileMap); break; case ESM::REC_NPC_: - readReferenceCollection (reader, mNpcs, contentFileMap); + readReferenceCollection (reader, mNpcs, cref, contentFileMap); break; case ESM::REC_PROB: - readReferenceCollection (reader, mProbes, contentFileMap); + readReferenceCollection (reader, mProbes, cref, contentFileMap); break; case ESM::REC_REPA: - readReferenceCollection (reader, mRepairs, contentFileMap); + readReferenceCollection (reader, mRepairs, cref, contentFileMap); break; case ESM::REC_STAT: - readReferenceCollection (reader, mStatics, contentFileMap); + readReferenceCollection (reader, mStatics, cref, contentFileMap); break; case ESM::REC_WEAP: - readReferenceCollection (reader, mWeapons, contentFileMap); + readReferenceCollection (reader, mWeapons, cref, contentFileMap); break; default: diff --git a/apps/openmw/mwworld/cellstore.hpp b/apps/openmw/mwworld/cellstore.hpp index ba6fad7ba..d7036d6b1 100644 --- a/apps/openmw/mwworld/cellstore.hpp +++ b/apps/openmw/mwworld/cellstore.hpp @@ -3,13 +3,15 @@ #include #include +#include +#include #include #include "livecellref.hpp" -#include "esmstore.hpp" #include "cellreflist.hpp" #include +#include #include "../mwmechanics/pathgrid.hpp" // TODO: maybe belongs in mwworld @@ -24,7 +26,7 @@ namespace ESM namespace MWWorld { class Ptr; - + class ESMStore; /// \brief Mutable state of a cell @@ -149,6 +151,17 @@ namespace MWWorld forEachImp (functor, mCreatureLists); } + template + bool forEachContainer (Functor& functor) + { + mHasState = true; + + return + forEachImp (functor, mContainers) && + forEachImp (functor, mCreatures) && + forEachImp (functor, mNpcs); + } + bool isExterior() const; Ptr searchInContainer (const std::string& id); @@ -170,7 +183,12 @@ namespace MWWorld template CellRefList& get() { - throw std::runtime_error ("Storage for this type not exist in cells"); + throw std::runtime_error ("Storage for type " + std::string(typeid(T).name())+ " does not exist in cells"); + } + + template + const CellRefList& getReadOnly() { + throw std::runtime_error ("Read Only CellRefList access not available for type " + std::string(typeid(T).name()) ); } bool isPointConnected(const int start, const int end) const; @@ -185,7 +203,7 @@ namespace MWWorld for (typename List::List::iterator iter (list.mList.begin()); iter!=list.mList.end(); ++iter) { - if (!iter->mData.getCount()) + if (iter->mData.isDeletedByContentFile()) continue; if (!functor (MWWorld::Ptr(&*iter, this))) return false; @@ -346,6 +364,12 @@ namespace MWWorld return mWeapons; } + template<> + inline const CellRefList& CellStore::getReadOnly() + { + return mDoors; + } + bool operator== (const CellStore& left, const CellStore& right); bool operator!= (const CellStore& left, const CellStore& right); } diff --git a/apps/openmw/mwworld/class.cpp b/apps/openmw/mwworld/class.cpp index 446519b87..6fa9ba9b6 100644 --- a/apps/openmw/mwworld/class.cpp +++ b/apps/openmw/mwworld/class.cpp @@ -10,6 +10,7 @@ #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" +#include "../mwworld/esmstore.hpp" #include "ptr.hpp" #include "refdata.hpp" @@ -37,12 +38,12 @@ namespace MWWorld throw std::runtime_error ("class does not support ID retrieval"); } - void Class::insertObjectRendering (const Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const + void Class::insertObjectRendering (const Ptr& ptr, const std::string& mesh, MWRender::RenderingInterface& renderingInterface) const { } - void Class::insertObject(const Ptr& ptr, MWWorld::PhysicsSystem& physics) const + void Class::insertObject(const Ptr& ptr, const std::string& mesh, MWWorld::PhysicsSystem& physics) const { } @@ -52,7 +53,7 @@ namespace MWWorld return false; } - void Class::skillUsageSucceeded (const MWWorld::Ptr& ptr, int skill, int usageType) const + void Class::skillUsageSucceeded (const MWWorld::Ptr& ptr, int skill, int usageType, float extraFactor) const { throw std::runtime_error ("class does not represent an actor"); } @@ -180,11 +181,6 @@ namespace MWWorld throw std::runtime_error ("class does not support enchanting"); } - float Class::getFallDamage(const MWWorld::Ptr &ptr, float fallHeight) const - { - return 0; - } - MWMechanics::Movement& Class::getMovementSettings (const Ptr& ptr) const { throw std::runtime_error ("movement settings not supported by class"); @@ -303,10 +299,6 @@ namespace MWWorld { } - void Class::adjustRotation(const MWWorld::Ptr& ptr,float& x,float& y,float& z) const - { - } - std::string Class::getModel(const MWWorld::Ptr &ptr) const { return ""; @@ -322,7 +314,7 @@ namespace MWWorld return std::make_pair (1, ""); } - void Class::adjustPosition(const MWWorld::Ptr& ptr) const + void Class::adjustPosition(const MWWorld::Ptr& ptr, bool force) const { } @@ -358,7 +350,7 @@ namespace MWWorld Class::copyToCell(const Ptr &ptr, CellStore &cell) const { Ptr newPtr = copyToCellImpl(ptr, cell); - + newPtr.getCellRef().unsetRefNum(); // This RefNum is only valid within the original cell of the reference return newPtr; } @@ -391,6 +383,16 @@ namespace MWWorld return false; } + bool Class::isPureWaterCreature(const MWWorld::Ptr& ptr) const + { + return canSwim(ptr) && !canWalk(ptr); + } + + bool Class::isMobile(const MWWorld::Ptr& ptr) const + { + return canSwim(ptr) || canWalk(ptr) || canFly(ptr); + } + int Class::getSkill(const MWWorld::Ptr& ptr, int skill) const { throw std::runtime_error("class does not support skills"); @@ -424,4 +426,37 @@ namespace MWWorld { throw std::runtime_error("this is not a door"); } + + float Class::getNormalizedEncumbrance(const Ptr &ptr) const + { + float capacity = getCapacity(ptr); + if (capacity == 0) + return 1.f; + + return getEncumbrance(ptr) / capacity; + } + + std::string Class::getSound(const MWWorld::Ptr&) const + { + return std::string(); + } + + int Class::getBaseFightRating(const Ptr &ptr) const + { + throw std::runtime_error("class does not support fight rating"); + } + + std::string Class::getPrimaryFaction (const MWWorld::Ptr& ptr) const + { + return std::string(); + } + int Class::getPrimaryFactionRank (const MWWorld::Ptr& ptr) const + { + return -1; + } + + int Class::getEffectiveArmorRating(const Ptr &ptr, const Ptr &actor) const + { + throw std::runtime_error("class does not support armor ratings"); + } } diff --git a/apps/openmw/mwworld/class.hpp b/apps/openmw/mwworld/class.hpp index c3f94d7f1..782aa7815 100644 --- a/apps/openmw/mwworld/class.hpp +++ b/apps/openmw/mwworld/class.hpp @@ -43,7 +43,6 @@ namespace ESM namespace MWWorld { - class Ptr; class ContainerStore; class InventoryStore; class PhysicsSystem; @@ -72,12 +71,6 @@ namespace MWWorld public: - /// NPC-stances. - enum Stance - { - Run, Sneak - }; - virtual ~Class(); const std::string& getTypeName() const { @@ -87,21 +80,24 @@ namespace MWWorld virtual std::string getId (const Ptr& ptr) const; ///< Return ID of \a ptr or throw an exception, if class does not support ID retrieval /// (default implementation: throw an exception) + /// @note This function is currently redundant; the same ID can be retrieved by CellRef::getRefId. + /// Leaving it here for now in case we want to optimize later. - virtual void insertObjectRendering (const Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const; - virtual void insertObject(const Ptr& ptr, MWWorld::PhysicsSystem& physics) const; + virtual void insertObjectRendering (const Ptr& ptr, const std::string& mesh, MWRender::RenderingInterface& renderingInterface) const; + virtual void insertObject(const Ptr& ptr, const std::string& mesh, MWWorld::PhysicsSystem& physics) const; ///< Add reference into a cell for rendering (default implementation: don't render anything). virtual std::string getName (const Ptr& ptr) const = 0; ///< \return name (the one that is to be presented to the user; not the internal one); /// can return an empty string. - virtual void adjustPosition(const MWWorld::Ptr& ptr) const; + virtual void adjustPosition(const MWWorld::Ptr& ptr, bool force) const; ///< Adjust position to stand on ground. Must be called post model load + /// @param force do this even if the ptr is flying virtual MWMechanics::CreatureStats& getCreatureStats (const Ptr& ptr) const; ///< Return creature stats or throw an exception, if class does not have creature stats - /// (default implementation: throw an exceoption) + /// (default implementation: throw an exception) virtual bool hasToolTip (const Ptr& ptr) const; ///< @return true if this object has a tooltip when focused (default implementation: false) @@ -111,7 +107,7 @@ namespace MWWorld virtual MWMechanics::NpcStats& getNpcStats (const Ptr& ptr) const; ///< Return NPC stats or throw an exception, if class does not have NPC stats - /// (default implementation: throw an exceoption) + /// (default implementation: throw an exception) virtual bool hasItemHealth (const Ptr& ptr) const; ///< \return Item health data available? (default implementation: false) @@ -128,7 +124,7 @@ namespace MWWorld /// of the given attacker, and whoever is hit. /// \param type - type of attack, one of the MWMechanics::CreatureStats::AttackType /// enums. ignored for creature attacks. - /// (default implementation: throw an exceoption) + /// (default implementation: throw an exception) virtual void onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, bool successful) const; ///< Alerts \a ptr that it's being hit for \a damage points to health if \a ishealth is @@ -144,7 +140,7 @@ namespace MWWorld ///< Sets a new current health value for the actor, optionally specifying the object causing /// the change. Use this instead of using CreatureStats directly as this will make sure the /// correct dialog and actor states are properly handled when being hurt or healed. - /// (default implementation: throw an exceoption) + /// (default implementation: throw an exception) virtual boost::shared_ptr activate (const Ptr& ptr, const Ptr& actor) const; ///< Generate action for activation (default implementation: return a null action). @@ -156,11 +152,11 @@ namespace MWWorld virtual ContainerStore& getContainerStore (const Ptr& ptr) const; ///< Return container store or throw an exception, if class does not have a - /// container store (default implementation: throw an exceoption) + /// container store (default implementation: throw an exception) virtual InventoryStore& getInventoryStore (const Ptr& ptr) const; ///< Return inventory store or throw an exception, if class does not have a - /// inventory store (default implementation: throw an exceoption) + /// inventory store (default implementation: throw an exception) virtual bool hasInventoryStore (const Ptr& ptr) const; ///< Does this object have an inventory store, i.e. equipment slots? (default implementation: false) @@ -189,9 +185,6 @@ namespace MWWorld virtual float getJump(const MWWorld::Ptr &ptr) const; ///< Return jump velocity (not accounting for movement) - virtual float getFallDamage(const MWWorld::Ptr &ptr, float fallHeight) const; - ///< Return amount of health points lost when falling - virtual MWMechanics::Movement& getMovementSettings (const Ptr& ptr) const; ///< Return desired movement. @@ -228,6 +221,9 @@ namespace MWWorld /// effects). Throws an exception, if the object can't hold other objects. /// (default implementation: throws an exception) + virtual float getNormalizedEncumbrance (const MWWorld::Ptr& ptr) const; + ///< Returns encumbrance re-scaled to capacity + virtual bool apply (const MWWorld::Ptr& ptr, const std::string& id, const MWWorld::Ptr& actor) const; ///< Apply \a id on \a ptr. @@ -236,7 +232,7 @@ namespace MWWorld /// /// (default implementation: ignore and return false) - virtual void skillUsageSucceeded (const MWWorld::Ptr& ptr, int skill, int usageType) const; + virtual void skillUsageSucceeded (const MWWorld::Ptr& ptr, int skill, int usageType, float extraFactor=1.f) const; ///< Inform actor \a ptr that a skill use has succeeded. /// /// (default implementations: throws an exception) @@ -272,8 +268,6 @@ namespace MWWorld virtual void adjustScale(const MWWorld::Ptr& ptr,float& scale) const; - virtual void adjustRotation(const MWWorld::Ptr& ptr,float& x,float& y,float& z) const; - virtual bool canSell (const MWWorld::Ptr& item, int npcServices) const; ///< Determine whether or not \a item can be sold to an npc with the given \a npcServices @@ -315,6 +309,8 @@ namespace MWWorld virtual bool canFly(const MWWorld::Ptr& ptr) const; virtual bool canSwim(const MWWorld::Ptr& ptr) const; virtual bool canWalk(const MWWorld::Ptr& ptr) const; + bool isPureWaterCreature(const MWWorld::Ptr& ptr) const; + bool isMobile(const MWWorld::Ptr& ptr) const; virtual int getSkill(const MWWorld::Ptr& ptr, int skill) const; @@ -343,6 +339,17 @@ namespace MWWorld virtual void respawn (const MWWorld::Ptr& ptr) const {} virtual void restock (const MWWorld::Ptr& ptr) const {} + + /// Returns sound id + virtual std::string getSound(const MWWorld::Ptr& ptr) const; + + virtual int getBaseFightRating (const MWWorld::Ptr& ptr) const; + + virtual std::string getPrimaryFaction (const MWWorld::Ptr& ptr) const; + virtual int getPrimaryFactionRank (const MWWorld::Ptr& ptr) const; + + /// Get the effective armor rating, factoring in the actor's skills, for the given armor. + virtual int getEffectiveArmorRating(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const; }; } diff --git a/apps/openmw/mwworld/containerstore.cpp b/apps/openmw/mwworld/containerstore.cpp index 18ebd82db..c2906e447 100644 --- a/apps/openmw/mwworld/containerstore.cpp +++ b/apps/openmw/mwworld/containerstore.cpp @@ -79,6 +79,14 @@ MWWorld::ContainerStoreIterator MWWorld::ContainerStore::getState (CellRefList void MWWorld::ContainerStore::storeState (const LiveCellRef& ref, ESM::ObjectState& state) const { @@ -86,8 +94,8 @@ void MWWorld::ContainerStore::storeState (const LiveCellRef& ref, ESM::Object } template -void MWWorld::ContainerStore::storeStates (const CellRefList& collection, - std::vector > >& states, bool equipable) const +void MWWorld::ContainerStore::storeStates (CellRefList& collection, + ESM::InventoryState& inventory, int& index, bool equipable) const { for (typename CellRefList::List::const_iterator iter (collection.mList.begin()); iter!=collection.mList.end(); ++iter) @@ -96,18 +104,13 @@ void MWWorld::ContainerStore::storeStates (const CellRefList& collection, continue; ESM::ObjectState state; storeState (*iter, state); - int slot = equipable ? getSlot (*iter) : -1; - states.push_back (std::make_pair (state, std::make_pair (T::sRecordId, slot))); + if (equipable) + storeEquipmentState(*iter, index, inventory); + inventory.mItems.push_back (state); + ++index; } } -int MWWorld::ContainerStore::getSlot (const MWWorld::LiveCellRefBase& ref) const -{ - return -1; -} - -void MWWorld::ContainerStore::setSlot (const MWWorld::ContainerStoreIterator& iter, int slot) {} - const std::string MWWorld::ContainerStore::sGoldId = "gold_001"; MWWorld::ContainerStore::ContainerStore() : mCachedWeight (0), mWeightUpToDate (false) {} @@ -219,29 +222,22 @@ MWWorld::ContainerStoreIterator MWWorld::ContainerStore::add (const Ptr& itemPtr MWWorld::ContainerStoreIterator it = end(); - if (setOwner && actorPtr.getClass().isActor()) + // HACK: Set owner on the original item, then reset it after we have copied it + // If we set the owner on the copied item, it would not stack correctly... + std::string oldOwner = itemPtr.getCellRef().getOwner(); + if (!setOwner || actorPtr == MWBase::Environment::get().getWorld()->getPlayerPtr()) // No point in setting owner to the player - NPCs will not respect this anyway { - // HACK: Set owner on the original item, then reset it after we have copied it - // If we set the owner on the copied item, it would not stack correctly... - std::string oldOwner = itemPtr.getCellRef().getOwner(); - if (actorPtr == player) - { - // No point in setting owner to the player - NPCs will not respect this anyway - // Additionally, setting it to "player" would make those items not stack with items that don't have an owner - itemPtr.getCellRef().setOwner(""); - } - else - itemPtr.getCellRef().setOwner(actorPtr.getCellRef().getRefId()); - - it = addImp(itemPtr, count); - - itemPtr.getCellRef().setOwner(oldOwner); + itemPtr.getCellRef().setOwner(""); } else { - it = addImp(itemPtr, count); + itemPtr.getCellRef().setOwner(actorPtr.getCellRef().getRefId()); } + it = addImp(itemPtr, count); + + itemPtr.getCellRef().setOwner(oldOwner); + // The copy of the original item we just made MWWorld::Ptr item = *it; @@ -256,6 +252,15 @@ MWWorld::ContainerStoreIterator MWWorld::ContainerStore::add (const Ptr& itemPtr pos.pos[2] = 0; item.getCellRef().setPosition(pos); + // reset ownership stuff, owner was already handled above + item.getCellRef().resetGlobalVariable(); + item.getCellRef().setFaction(""); + item.getCellRef().setFactionRank(-1); + + // must reset the RefNum on the copied item, so that the RefNum on the original item stays unique + // maybe we should do this in the copy constructor instead? + item.getCellRef().unsetRefNum(); // destroy link to content file + std::string script = item.getClass().getScript(item); if(script != "") { @@ -271,7 +276,7 @@ MWWorld::ContainerStoreIterator MWWorld::ContainerStore::add (const Ptr& itemPtr item.mCell = actorPtr.getCell(); } - item.mContainerStore = 0; + item.mContainerStore = this; MWBase::Environment::get().getWorld()->getLocalScripts().add(script, item); @@ -395,19 +400,19 @@ int MWWorld::ContainerStore::remove(const Ptr& item, int count, const Ptr& actor return count - toRemove; } -void MWWorld::ContainerStore::fill (const ESM::InventoryList& items, const std::string& owner, const std::string& faction, const MWWorld::ESMStore& store) +void MWWorld::ContainerStore::fill (const ESM::InventoryList& items, const std::string& owner) { for (std::vector::const_iterator iter (items.mList.begin()); iter!=items.mList.end(); ++iter) { std::string id = Misc::StringUtils::lowerCase(iter->mItem.toString()); - addInitialItem(id, owner, faction, iter->mCount); + addInitialItem(id, owner, iter->mCount); } flagAsModified(); } -void MWWorld::ContainerStore::addInitialItem (const std::string& id, const std::string& owner, const std::string& faction, +void MWWorld::ContainerStore::addInitialItem (const std::string& id, const std::string& owner, int count, bool topLevel, const std::string& levItem) { ManualRef ref (MWBase::Environment::get().getWorld()->getStore(), id, count); @@ -419,7 +424,7 @@ void MWWorld::ContainerStore::addInitialItem (const std::string& id, const std:: if (topLevel && std::abs(count) > 1 && levItem->mFlags & ESM::ItemLevList::Each) { for (int i=0; i 0 ? 1 : -1, true, levItem->mId); + addInitialItem(id, owner, count > 0 ? 1 : -1, true, levItem->mId); return; } else @@ -427,7 +432,7 @@ void MWWorld::ContainerStore::addInitialItem (const std::string& id, const std:: std::string id = MWMechanics::getLevelledItem(ref.getPtr().get()->mBase, false); if (id.empty()) return; - addInitialItem(id, owner, faction, count, false, levItem->mId); + addInitialItem(id, owner, count, false, levItem->mId); } } else @@ -443,12 +448,11 @@ void MWWorld::ContainerStore::addInitialItem (const std::string& id, const std:: count = std::abs(count); ref.getPtr().getCellRef().setOwner(owner); - ref.getPtr().getCellRef().setFaction(faction); addImp (ref.getPtr(), count); } } -void MWWorld::ContainerStore::restock (const ESM::InventoryList& items, const MWWorld::Ptr& ptr, const std::string& owner, const std::string& faction) +void MWWorld::ContainerStore::restock (const ESM::InventoryList& items, const MWWorld::Ptr& ptr, const std::string& owner) { // Remove the items already spawned by levelled items that will restock for (std::map::iterator it = mLevelledItemMap.begin(); it != mLevelledItemMap.end(); ++it) @@ -467,13 +471,13 @@ void MWWorld::ContainerStore::restock (const ESM::InventoryList& items, const MW if (MWBase::Environment::get().getWorld()->getStore().get().search(it->mItem.toString())) { - addInitialItem(item, owner, faction, it->mCount, true); + addInitialItem(item, owner, it->mCount, true); } else { int currentCount = count(item); if (currentCount < std::abs(it->mCount)) - add (item, std::abs(it->mCount) - currentCount, ptr); + addInitialItem(item, owner, std::abs(it->mCount) - currentCount, true); } } flagAsModified(); @@ -639,71 +643,66 @@ MWWorld::Ptr MWWorld::ContainerStore::search (const std::string& id) return Ptr(); } -void MWWorld::ContainerStore::writeState (ESM::InventoryState& state) const +void MWWorld::ContainerStore::writeState (ESM::InventoryState& state) { state.mItems.clear(); - storeStates (potions, state.mItems); - storeStates (appas, state.mItems); - storeStates (armors, state.mItems, true); - storeStates (books, state.mItems); - storeStates (clothes, state.mItems, true); - storeStates (ingreds, state.mItems); - storeStates (lockpicks, state.mItems, true); - storeStates (miscItems, state.mItems); - storeStates (probes, state.mItems, true); - storeStates (repairs, state.mItems); - storeStates (weapons, state.mItems, true); - - state.mLights.clear(); + int index = 0; + storeStates (potions, state, index); + storeStates (appas, state, index); + storeStates (armors, state, index, true); + storeStates (books, state, index, true); // not equipable as such, but for selectedEnchantItem + storeStates (clothes, state, index, true); + storeStates (ingreds, state, index); + storeStates (lockpicks, state, index, true); + storeStates (miscItems, state, index); + storeStates (probes, state, index, true); + storeStates (repairs, state, index); + storeStates (weapons, state, index, true); + storeStates (lights, state, index, true); state.mLevelledItemMap = mLevelledItemMap; - - for (MWWorld::CellRefList::List::const_iterator iter (lights.mList.begin()); - iter!=lights.mList.end(); ++iter) - { - ESM::LightState objectState; - storeState (*iter, objectState); - state.mLights.push_back (std::make_pair (objectState, getSlot (*iter))); - } } -void MWWorld::ContainerStore::readState (const ESM::InventoryState& state) +void MWWorld::ContainerStore::readState (const ESM::InventoryState& inventory) { clear(); - for (std::vector > >::const_iterator - iter (state.mItems.begin()); iter!=state.mItems.end(); ++iter) + int index = 0; + for (std::vector::const_iterator + iter (inventory.mItems.begin()); iter!=inventory.mItems.end(); ++iter) { - int slot = iter->second.second; + const ESM::ObjectState& state = *iter; - switch (iter->second.first) - { - case ESM::REC_ALCH: getState (potions, iter->first); break; - case ESM::REC_APPA: getState (appas, iter->first); break; - case ESM::REC_ARMO: setSlot (getState (armors, iter->first), slot); break; - case ESM::REC_BOOK: getState (books, iter->first); break; - case ESM::REC_CLOT: setSlot (getState (clothes, iter->first), slot); break; - case ESM::REC_INGR: getState (ingreds, iter->first); break; - case ESM::REC_LOCK: setSlot (getState (lockpicks, iter->first), slot); break; - case ESM::REC_MISC: getState (miscItems, iter->first); break; - case ESM::REC_PROB: setSlot (getState (probes, iter->first), slot); break; - case ESM::REC_REPA: getState (repairs, iter->first); break; - case ESM::REC_WEAP: setSlot (getState (weapons, iter->first), slot); break; + int type = MWBase::Environment::get().getWorld()->getStore().find(state.mRef.mRefID); - default: + int thisIndex = index++; - std::cerr << "invalid item type in inventory state" << std::endl; + switch (type) + { + case ESM::REC_ALCH: getState (potions, state); break; + case ESM::REC_APPA: getState (appas, state); break; + case ESM::REC_ARMO: readEquipmentState (getState (armors, state), thisIndex, inventory); break; + case ESM::REC_BOOK: readEquipmentState (getState (books, state), thisIndex, inventory); break; // not equipable as such, but for selectedEnchantItem + case ESM::REC_CLOT: readEquipmentState (getState (clothes, state), thisIndex, inventory); break; + case ESM::REC_INGR: getState (ingreds, state); break; + case ESM::REC_LOCK: readEquipmentState (getState (lockpicks, state), thisIndex, inventory); break; + case ESM::REC_MISC: getState (miscItems, state); break; + case ESM::REC_PROB: readEquipmentState (getState (probes, state), thisIndex, inventory); break; + case ESM::REC_REPA: getState (repairs, state); break; + case ESM::REC_WEAP: readEquipmentState (getState (weapons, state), thisIndex, inventory); break; + case ESM::REC_LIGH: readEquipmentState (getState (lights, state), thisIndex, inventory); break; + case 0: + std::cerr << "Dropping reference to '" << state.mRef.mRefID << "' (object no longer exists)" << std::endl; + break; + default: + std::cerr << "Invalid item type in inventory state, refid " << state.mRef.mRefID << std::endl; + break; } } - for (std::vector >::const_iterator iter (state.mLights.begin()); - iter!=state.mLights.end(); ++iter) - { - getState (lights, iter->first); - } - mLevelledItemMap = state.mLevelledItemMap; + mLevelledItemMap = inventory.mLevelledItemMap; } diff --git a/apps/openmw/mwworld/containerstore.hpp b/apps/openmw/mwworld/containerstore.hpp index 6d9d7a6bb..e9750a622 100644 --- a/apps/openmw/mwworld/containerstore.hpp +++ b/apps/openmw/mwworld/containerstore.hpp @@ -18,6 +18,7 @@ #include #include "ptr.hpp" +#include "cellreflist.hpp" namespace ESM { @@ -74,7 +75,7 @@ namespace MWWorld mutable float mCachedWeight; mutable bool mWeightUpToDate; ContainerStoreIterator addImp (const Ptr& ptr, int count); - void addInitialItem (const std::string& id, const std::string& owner, const std::string& faction, int count, bool topLevel=true, const std::string& levItem = ""); + void addInitialItem (const std::string& id, const std::string& owner, int count, bool topLevel=true, const std::string& levItem = ""); template ContainerStoreIterator getState (CellRefList& collection, @@ -84,16 +85,13 @@ namespace MWWorld void storeState (const LiveCellRef& ref, ESM::ObjectState& state) const; template - void storeStates (const CellRefList& collection, - std::vector > >& states, + void storeStates (CellRefList& collection, + ESM::InventoryState& inventory, int& index, bool equipable = false) const; - virtual int getSlot (const MWWorld::LiveCellRefBase& ref) const; - ///< Return inventory slot that \a ref is in or -1 (if \a ref is not in a slot). - - virtual void setSlot (const MWWorld::ContainerStoreIterator& iter, int slot); - ///< Set slot for \a iter. Ignored if \a iter is an end iterator or if slot==-1. + virtual void storeEquipmentState (const MWWorld::LiveCellRefBase& ref, int index, ESM::InventoryState& inventory) const; + virtual void readEquipmentState (const MWWorld::ContainerStoreIterator& iter, int index, const ESM::InventoryState& inventory); public: ContainerStore(); @@ -114,7 +112,7 @@ namespace MWWorld /// \attention Do not add items to an existing stack by increasing the count instead of /// calling this function! /// - /// @param setOwner Set the owner of the added item to \a actorPtr? + /// @param setOwner Set the owner of the added item to \a actorPtr? If false, the owner is reset to "". /// /// @return if stacking happened, return iterator to the item that was stacked against, otherwise iterator to the newly inserted item. @@ -153,10 +151,10 @@ namespace MWWorld virtual bool stacks (const Ptr& ptr1, const Ptr& ptr2); ///< @return true if the two specified objects can stack with each other - void fill (const ESM::InventoryList& items, const std::string& owner, const std::string& faction, const MWWorld::ESMStore& store); + void fill (const ESM::InventoryList& items, const std::string& owner); ///< Insert items into *this. - void restock (const ESM::InventoryList& items, const MWWorld::Ptr& ptr, const std::string& owner, const std::string& faction); + void restock (const ESM::InventoryList& items, const MWWorld::Ptr& ptr, const std::string& owner); virtual void clear(); ///< Empty container. @@ -170,9 +168,10 @@ namespace MWWorld Ptr search (const std::string& id); - void writeState (ESM::InventoryState& state) const; + /// \todo make this method const once const-correct ContainerStoreIterators are available + virtual void writeState (ESM::InventoryState& state); - void readState (const ESM::InventoryState& state); + virtual void readState (const ESM::InventoryState& state); friend class ContainerStoreIterator; }; diff --git a/apps/openmw/mwworld/esmloader.cpp b/apps/openmw/mwworld/esmloader.cpp index 1b8880d37..13a786d00 100644 --- a/apps/openmw/mwworld/esmloader.cpp +++ b/apps/openmw/mwworld/esmloader.cpp @@ -1,7 +1,7 @@ #include "esmloader.hpp" #include "esmstore.hpp" -#include "components/to_utf8/to_utf8.hpp" +#include namespace MWWorld { diff --git a/apps/openmw/mwworld/esmloader.hpp b/apps/openmw/mwworld/esmloader.hpp index d799c3f15..b96af707c 100644 --- a/apps/openmw/mwworld/esmloader.hpp +++ b/apps/openmw/mwworld/esmloader.hpp @@ -4,13 +4,17 @@ #include #include "contentloader.hpp" -#include "components/esm/esmreader.hpp" namespace ToUTF8 { class Utf8Encoder; } +namespace ESM +{ + class ESMReader; +} + namespace MWWorld { diff --git a/apps/openmw/mwworld/esmstore.cpp b/apps/openmw/mwworld/esmstore.cpp index 12831e7dc..36c198f01 100644 --- a/apps/openmw/mwworld/esmstore.cpp +++ b/apps/openmw/mwworld/esmstore.cpp @@ -7,6 +7,8 @@ #include +#include + namespace MWWorld { @@ -81,7 +83,13 @@ void ESMStore::load(ESM::ESMReader &esm, Loading::Listener* listener) mMagicEffects.load (esm); } else if (n.val == ESM::REC_SKIL) { mSkills.load (esm); - } else { + } + else if (n.val==ESM::REC_FILT || n.val == ESM::REC_DBGP) + { + // ignore project file only records + esm.skipRecord(); + } + else { std::stringstream error; error << "Unknown record: " << n.toString(); throw std::runtime_error(error.str()); @@ -129,6 +137,7 @@ void ESMStore::setUp() mSkills.setUp(); mMagicEffects.setUp(); mAttributes.setUp(); + mDialogs.setUp(); } int ESMStore::countSavedGameRecords() const @@ -142,7 +151,9 @@ void ESMStore::setUp() +mEnchants.getDynamicSize() +mNpcs.getDynamicSize() +mSpells.getDynamicSize() - +mWeapons.getDynamicSize(); + +mWeapons.getDynamicSize() + +mCreatureLists.getDynamicSize() + +mItemLists.getDynamicSize(); } void ESMStore::write (ESM::ESMWriter& writer, Loading::Listener& progress) const @@ -152,7 +163,6 @@ void ESMStore::setUp() writer.writeT(mDynamicCount); writer.endRecord("COUN"); writer.endRecord(ESM::REC_DYNA); - progress.increaseProgress(); mPotions.write (writer, progress); mArmors.write (writer, progress); @@ -163,9 +173,11 @@ void ESMStore::setUp() mSpells.write (writer, progress); mWeapons.write (writer, progress); mNpcs.write (writer, progress); + mItemLists.write (writer, progress); + mCreatureLists.write (writer, progress); } - bool ESMStore::readRecord (ESM::ESMReader& reader, int32_t type) + bool ESMStore::readRecord (ESM::ESMReader& reader, uint32_t type) { switch (type) { @@ -178,8 +190,18 @@ void ESMStore::setUp() case ESM::REC_SPEL: case ESM::REC_WEAP: case ESM::REC_NPC_: + case ESM::REC_LEVI: + case ESM::REC_LEVC: - mStores[type]->read (reader); + { + std::string id = reader.getHNString ("NAME"); + mStores[type]->read (reader, id); + + // FIXME: there might be stale dynamic IDs in mIds from an earlier savegame + // that really should be cleared instead of just overwritten + + mIds[id] = type; + } if (type==ESM::REC_NPC_) { diff --git a/apps/openmw/mwworld/esmstore.hpp b/apps/openmw/mwworld/esmstore.hpp index 90786acd4..05b633956 100644 --- a/apps/openmw/mwworld/esmstore.hpp +++ b/apps/openmw/mwworld/esmstore.hpp @@ -99,9 +99,6 @@ namespace MWWorld ESMStore() : mDynamicCount(0) { - // Cell store needs access to this for tracking moved references - mCells.mEsmStore = this; - mStores[ESM::REC_ACTI] = &mActivators; mStores[ESM::REC_ALCH] = &mPotions; mStores[ESM::REC_APPA] = &mAppas; @@ -141,6 +138,8 @@ namespace MWWorld mStores[ESM::REC_SSCR] = &mStartScripts; mStores[ESM::REC_STAT] = &mStatics; mStores[ESM::REC_WEAP] = &mWeapons; + + mPathgrids.setCells(mCells); } void clearDynamic () @@ -165,6 +164,7 @@ namespace MWWorld throw std::runtime_error("Storage for this type not exist"); } + /// Insert a custom record (i.e. with a generated ID that will not clash will pre-existing records) template const T *insert(const T &x) { std::ostringstream id; @@ -189,6 +189,20 @@ namespace MWWorld return ptr; } + /// Insert a record with set ID, and allow it to override a pre-existing static record. + template + const T *overrideRecord(const T &x) { + Store &store = const_cast &>(get()); + + T *ptr = store.insert(x); + for (iterator it = mStores.begin(); it != mStores.end(); ++it) { + if (it->second == &store) { + mIds[ptr->mId] = it->first; + } + } + return ptr; + } + template const T *insertStatic(const T &x) { std::ostringstream id; @@ -219,7 +233,7 @@ namespace MWWorld void write (ESM::ESMWriter& writer, Loading::Listener& progress) const; - bool readRecord (ESM::ESMReader& reader, int32_t type); + bool readRecord (ESM::ESMReader& reader, uint32_t type); ///< \return Known type? }; diff --git a/apps/openmw/mwworld/fallback.cpp b/apps/openmw/mwworld/fallback.cpp index c0b21b2ef..3a8154ca7 100644 --- a/apps/openmw/mwworld/fallback.cpp +++ b/apps/openmw/mwworld/fallback.cpp @@ -22,6 +22,15 @@ namespace MWWorld else return boost::lexical_cast(fallback); } + int Fallback::getFallbackInt(const std::string& fall) const + { + std::string fallback=getFallbackString(fall); + if(fallback.empty()) + return 0; + else + return boost::lexical_cast(fallback); + } + bool Fallback::getFallbackBool(const std::string& fall) const { std::string fallback=getFallbackString(fall); diff --git a/apps/openmw/mwworld/fallback.hpp b/apps/openmw/mwworld/fallback.hpp index 6c5802e07..f69a5e57b 100644 --- a/apps/openmw/mwworld/fallback.hpp +++ b/apps/openmw/mwworld/fallback.hpp @@ -15,6 +15,7 @@ namespace MWWorld Fallback(const std::map& fallback); std::string getFallbackString(const std::string& fall) const; float getFallbackFloat(const std::string& fall) const; + int getFallbackInt(const std::string& fall) const; bool getFallbackBool(const std::string& fall) const; Ogre::ColourValue getFallbackColour(const std::string& fall) const; }; diff --git a/apps/openmw/mwworld/globals.cpp b/apps/openmw/mwworld/globals.cpp index 15ba27498..e7cb04590 100644 --- a/apps/openmw/mwworld/globals.cpp +++ b/apps/openmw/mwworld/globals.cpp @@ -85,11 +85,10 @@ namespace MWWorld writer.writeHNString ("NAME", iter->first); iter->second.write (writer, ESM::Variant::Format_Global); writer.endRecord (ESM::REC_GLOB); - progress.increaseProgress(); } } - bool Globals::readRecord (ESM::ESMReader& reader, int32_t type) + bool Globals::readRecord (ESM::ESMReader& reader, uint32_t type) { if (type==ESM::REC_GLOB) { diff --git a/apps/openmw/mwworld/globals.hpp b/apps/openmw/mwworld/globals.hpp index 3ff4a5d6e..bb4ab13d9 100644 --- a/apps/openmw/mwworld/globals.hpp +++ b/apps/openmw/mwworld/globals.hpp @@ -53,7 +53,7 @@ namespace MWWorld void write (ESM::ESMWriter& writer, Loading::Listener& progress) const; - bool readRecord (ESM::ESMReader& reader, int32_t type); + bool readRecord (ESM::ESMReader& reader, uint32_t type); ///< Records for variables that do not exist are dropped silently. /// /// \return Known type? diff --git a/apps/openmw/mwworld/inventorystore.cpp b/apps/openmw/mwworld/inventorystore.cpp index 891440023..020f9561a 100644 --- a/apps/openmw/mwworld/inventorystore.cpp +++ b/apps/openmw/mwworld/inventorystore.cpp @@ -5,6 +5,7 @@ #include #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -48,19 +49,47 @@ void MWWorld::InventoryStore::initSlots (TSlots& slots_) slots_.push_back (end()); } -int MWWorld::InventoryStore::getSlot (const MWWorld::LiveCellRefBase& ref) const +void MWWorld::InventoryStore::storeEquipmentState(const MWWorld::LiveCellRefBase &ref, int index, ESM::InventoryState &inventory) const { for (int i = 0; i (mSlots.size()); ++i) if (mSlots[i].getType()!=-1 && mSlots[i]->getBase()==&ref) - return i; + { + inventory.mEquipmentSlots[index] = i; + } - return -1; + if (mSelectedEnchantItem.getType()!=-1 && mSelectedEnchantItem->getBase() == &ref) + inventory.mSelectedEnchantItem = index; } -void MWWorld::InventoryStore::setSlot (const MWWorld::ContainerStoreIterator& iter, int slot) +void MWWorld::InventoryStore::readEquipmentState(const MWWorld::ContainerStoreIterator &iter, int index, const ESM::InventoryState &inventory) { - if (iter!=end() && slot>=0 && slot::const_iterator found = inventory.mEquipmentSlots.find(index); + if (found != inventory.mEquipmentSlots.end()) + { + if (found->second < 0 || found->second >= MWWorld::InventoryStore::Slots) + throw std::runtime_error("Invalid slot index in inventory state"); + + // make sure the item can actually be equipped in this slot + int slot = found->second; + std::pair, bool> allowedSlots = iter->getClass().getEquipmentSlots(*iter); + if (!allowedSlots.first.size()) + return; + if (std::find(allowedSlots.first.begin(), allowedSlots.first.end(), slot) == allowedSlots.first.end()) + slot = allowedSlots.first.front(); + + // unstack if required + if (!allowedSlots.second && iter->getRefData().getCount() > 1) + { + MWWorld::ContainerStoreIterator newIter = addNewStack(*iter, 1); + iter->getRefData().setCount(iter->getRefData().getCount()-1); + mSlots[slot] = newIter; + } + else + mSlots[slot] = iter; + } } MWWorld::InventoryStore::InventoryStore() @@ -68,20 +97,21 @@ MWWorld::InventoryStore::InventoryStore() , mUpdatesEnabled (true) , mFirstAutoEquip(true) , mListener(NULL) + , mRechargingItemsUpToDate(false) { initSlots (mSlots); } MWWorld::InventoryStore::InventoryStore (const InventoryStore& store) -: ContainerStore (store) + : ContainerStore (store) , mSelectedEnchantItem(end()) + , mMagicEffects(store.mMagicEffects) + , mFirstAutoEquip(store.mFirstAutoEquip) + , mListener(store.mListener) + , mUpdatesEnabled(store.mUpdatesEnabled) + , mPermanentMagicEffectMagnitudes(store.mPermanentMagicEffectMagnitudes) + , mRechargingItemsUpToDate(false) { - mMagicEffects = store.mMagicEffects; - mFirstAutoEquip = store.mFirstAutoEquip; - mListener = store.mListener; - mUpdatesEnabled = store.mUpdatesEnabled; - - mPermanentMagicEffectMagnitudes = store.mPermanentMagicEffectMagnitudes; copySlots (store); } @@ -91,6 +121,7 @@ MWWorld::InventoryStore& MWWorld::InventoryStore::operator= (const InventoryStor mMagicEffects = store.mMagicEffects; mFirstAutoEquip = store.mFirstAutoEquip; mPermanentMagicEffectMagnitudes = store.mPermanentMagicEffectMagnitudes; + mRechargingItemsUpToDate = false; ContainerStore::operator= (store); mSlots.clear(); copySlots (store); @@ -102,17 +133,14 @@ MWWorld::ContainerStoreIterator MWWorld::InventoryStore::add(const Ptr& itemPtr, const MWWorld::ContainerStoreIterator& retVal = MWWorld::ContainerStore::add(itemPtr, count, actorPtr, setOwner); // Auto-equip items if an armor/clothing or weapon item is added, but not for the player nor werewolves - if ((actorPtr.getRefData().getHandle() != "player") - && !(actorPtr.getClass().isNpc() && actorPtr.getClass().getNpcStats(actorPtr).isWerewolf()) - && !actorPtr.getClass().getCreatureStats(actorPtr).isDead()) + if (actorPtr.getRefData().getHandle() != "player" + && !(actorPtr.getClass().isNpc() && actorPtr.getClass().getNpcStats(actorPtr).isWerewolf())) { std::string type = itemPtr.getTypeName(); - if ((type == typeid(ESM::Armor).name()) || (type == typeid(ESM::Clothing).name()) || (type == typeid(ESM::Weapon).name())) + if (type == typeid(ESM::Armor).name() || type == typeid(ESM::Clothing).name()) autoEquip(actorPtr); } - updateRechargingItems(); - return retVal; } @@ -148,16 +176,18 @@ void MWWorld::InventoryStore::equip (int slot, const ContainerStoreIterator& ite flagAsModified(); fireEquipmentChangedEvent(); + updateMagicEffects(actor); } void MWWorld::InventoryStore::unequipAll(const MWWorld::Ptr& actor) { - // Only *one* change event should be fired mUpdatesEnabled = false; for (int slot=0; slot < MWWorld::InventoryStore::Slots; ++slot) unequipSlot(slot, actor); + mUpdatesEnabled = true; + fireEquipmentChangedEvent(); updateMagicEffects(actor); } @@ -198,19 +228,17 @@ void MWWorld::InventoryStore::autoEquip (const MWWorld::Ptr& actor) continue; } - // Don't auto-equip probes or lockpicks. NPCs can't use them (yet). And AiCombat would attempt to "attack" with them. - // NOTE: In the future AiCombat should handle equipping appropriate weapons - if (test.getTypeName() == typeid(ESM::Lockpick).name() || test.getTypeName() == typeid(ESM::Probe).name()) - continue; - // Only autoEquip if we are the original owner of the item. // This stops merchants from auto equipping anything you sell to them. // ...unless this is a companion, he should always equip items given to him. if (!Misc::StringUtils::ciEqual(test.getCellRef().getOwner(), actor.getCellRef().getRefId()) && (actor.getClass().getScript(actor).empty() || - !actor.getRefData().getLocals().getIntVar(actor.getClass().getScript(actor), "companion"))) + !actor.getRefData().getLocals().getIntVar(actor.getClass().getScript(actor), "companion")) + && !actor.getClass().getCreatureStats(actor).isDead() // Corpses can be dressed up by the player as desired + ) + { continue; - + } int testSkill = test.getClass().getEquipmentSkill (test); std::pair, bool> itemsSlots = @@ -219,30 +247,27 @@ void MWWorld::InventoryStore::autoEquip (const MWWorld::Ptr& actor) for (std::vector::const_iterator iter2 (itemsSlots.first.begin()); iter2!=itemsSlots.first.end(); ++iter2) { - bool use = false; + if (*iter2 == Slot_CarriedRight) // Items in right hand are situational use, so don't equip them. + // Equipping weapons is handled by AiCombat. Anything else (lockpicks, probes) can't be used by NPCs anyway (yet) + continue; - if (slots_.at (*iter2)==end()) - use = true; // slot was empty before -> skip all further checks - else + if (slots_.at (*iter2)!=end()) { Ptr old = *slots_.at (*iter2); - if (!use) + // check skill + int oldSkill = old.getClass().getEquipmentSkill (old); + + bool use = false; + if (testSkill!=-1 && oldSkill==-1) + use = true; + else if (testSkill!=-1 && oldSkill!=-1 && testSkill!=oldSkill) { - // check skill - int oldSkill = - old.getClass().getEquipmentSkill (old); + if (actor.getClass().getSkill(actor, oldSkill) > actor.getClass().getSkill (actor, testSkill)) + continue; // rejected, because old item better matched the NPC's skills. - if (testSkill!=-1 && oldSkill==-1) + if (actor.getClass().getSkill(actor, oldSkill) < actor.getClass().getSkill (actor, testSkill)) use = true; - else if (testSkill!=-1 && oldSkill!=-1 && testSkill!=oldSkill) - { - if (actor.getClass().getSkill(actor, oldSkill) > actor.getClass().getSkill (actor, testSkill)) - continue; // rejected, because old item better matched the NPC's skills. - - if (actor.getClass().getSkill(actor, oldSkill) < actor.getClass().getSkill (actor, testSkill)) - use = true; - } } if (!use) @@ -253,8 +278,6 @@ void MWWorld::InventoryStore::autoEquip (const MWWorld::Ptr& actor) { continue; } - - use = true; } } @@ -262,11 +285,7 @@ void MWWorld::InventoryStore::autoEquip (const MWWorld::Ptr& actor) { case 0: continue; - case 2: - slots_[MWWorld::InventoryStore::Slot_CarriedLeft] = end(); - break; - case 3: - // Prefer keeping twohanded weapon + default: break; } @@ -287,11 +306,13 @@ void MWWorld::InventoryStore::autoEquip (const MWWorld::Ptr& actor) bool changed = false; for (std::size_t i=0; igetClass().getScript(*it); if (script != "") (*it).getRefData().getLocals().setVarByInt(script, "onpcequip", 0); + } - if ((mSelectedEnchantItem != end()) && (mSelectedEnchantItem == it)) - { - mSelectedEnchantItem = end(); - } + if ((mSelectedEnchantItem != end()) && (mSelectedEnchantItem == it)) + { + mSelectedEnchantItem = end(); } } @@ -592,7 +611,8 @@ void MWWorld::InventoryStore::visitEffectSources(MWMechanics::EffectSourceVisito const EffectParams& params = mPermanentMagicEffectMagnitudes[(**iter).getCellRef().getRefId()][i]; float magnitude = effectIt->mMagnMin + (effectIt->mMagnMax - effectIt->mMagnMin) * params.mRandom; magnitude *= params.mMultiplier; - visitor.visit(MWMechanics::EffectKey(*effectIt), (**iter).getClass().getName(**iter), -1, magnitude); + if (magnitude > 0) + visitor.visit(MWMechanics::EffectKey(*effectIt), (**iter).getClass().getName(**iter), (**iter).getCellRef().getRefId(), -1, magnitude); ++i; } @@ -617,6 +637,11 @@ void MWWorld::InventoryStore::updateRechargingItems() void MWWorld::InventoryStore::rechargeItems(float duration) { + if (!mRechargingItemsUpToDate) + { + updateRechargingItems(); + mRechargingItemsUpToDate = true; + } for (TRechargingItems::iterator it = mRechargingItems.begin(); it != mRechargingItems.end(); ++it) { if (it->first->getCellRef().getEnchantmentCharge() == -1 @@ -640,7 +665,53 @@ void MWWorld::InventoryStore::rechargeItems(float duration) void MWWorld::InventoryStore::purgeEffect(short effectId) { - mMagicEffects.add(MWMechanics::EffectKey(effectId), -mMagicEffects.get(MWMechanics::EffectKey(effectId)).mMagnitude); + mMagicEffects.remove(MWMechanics::EffectKey(effectId)); +} + +void MWWorld::InventoryStore::purgeEffect(short effectId, const std::string &sourceId) +{ + TEffectMagnitudes::iterator effectMagnitudeIt = mPermanentMagicEffectMagnitudes.find(sourceId); + if (effectMagnitudeIt == mPermanentMagicEffectMagnitudes.end()) + return; + + for (TSlots::const_iterator iter (mSlots.begin()); iter!=mSlots.end(); ++iter) + { + if (*iter==end()) + continue; + + if ((*iter)->getClass().getId(**iter) != sourceId) + continue; + + std::string enchantmentId = (*iter)->getClass().getEnchantment (**iter); + + if (!enchantmentId.empty()) + { + const ESM::Enchantment& enchantment = + *MWBase::Environment::get().getWorld()->getStore().get().find (enchantmentId); + + if (enchantment.mData.mType != ESM::Enchantment::ConstantEffect) + continue; + + std::vector& params = effectMagnitudeIt->second; + + int i=0; + for (std::vector::const_iterator effectIt (enchantment.mEffects.mList.begin()); + effectIt!=enchantment.mEffects.mList.end(); ++effectIt, ++i) + { + if (effectIt->mEffectID != effectId) + continue; + + float magnitude = effectIt->mMagnMin + (effectIt->mMagnMax - effectIt->mMagnMin) * params[i].mRandom; + magnitude *= params[i].mMultiplier; + + if (magnitude) + mMagicEffects.add (*effectIt, -magnitude); + + params[i].mMultiplier = 0; + break; + } + } + } } void MWWorld::InventoryStore::clear() @@ -649,3 +720,49 @@ void MWWorld::InventoryStore::clear() initSlots (mSlots); ContainerStore::clear(); } + +bool MWWorld::InventoryStore::isEquipped(const MWWorld::Ptr &item) +{ + for (int i=0; i < MWWorld::InventoryStore::Slots; ++i) + { + if (getSlot(i) != end() && *getSlot(i) == item) + return true; + } + return false; +} + +void MWWorld::InventoryStore::writeState(ESM::InventoryState &state) +{ + MWWorld::ContainerStore::writeState(state); + + for (TEffectMagnitudes::const_iterator it = mPermanentMagicEffectMagnitudes.begin(); it != mPermanentMagicEffectMagnitudes.end(); ++it) + { + std::vector > params; + for (std::vector::const_iterator pIt = it->second.begin(); pIt != it->second.end(); ++pIt) + { + params.push_back(std::make_pair(pIt->mRandom, pIt->mMultiplier)); + } + + state.mPermanentMagicEffectMagnitudes[it->first] = params; + } +} + +void MWWorld::InventoryStore::readState(const ESM::InventoryState &state) +{ + MWWorld::ContainerStore::readState(state); + + for (ESM::InventoryState::TEffectMagnitudes::const_iterator it = state.mPermanentMagicEffectMagnitudes.begin(); + it != state.mPermanentMagicEffectMagnitudes.end(); ++it) + { + std::vector params; + for (std::vector >::const_iterator pIt = it->second.begin(); pIt != it->second.end(); ++pIt) + { + EffectParams p; + p.mRandom = pIt->first; + p.mMultiplier = pIt->second; + params.push_back(p); + } + + mPermanentMagicEffectMagnitudes[it->first] = params; + } +} diff --git a/apps/openmw/mwworld/inventorystore.hpp b/apps/openmw/mwworld/inventorystore.hpp index 95b956907..9a154373a 100644 --- a/apps/openmw/mwworld/inventorystore.hpp +++ b/apps/openmw/mwworld/inventorystore.hpp @@ -102,6 +102,8 @@ namespace MWWorld typedef std::vector > TRechargingItems; TRechargingItems mRechargingItems; + bool mRechargingItemsUpToDate; + void copySlots (const InventoryStore& store); void initSlots (TSlots& slots_); @@ -111,11 +113,8 @@ namespace MWWorld void fireEquipmentChangedEvent(); - virtual int getSlot (const MWWorld::LiveCellRefBase& ref) const; - ///< Return inventory slot that \a ref is in or -1 (if \a ref is not in a slot). - - virtual void setSlot (const MWWorld::ContainerStoreIterator& iter, int slot); - ///< Set slot for \a iter. Ignored if \a iter is an end iterator or if slot==-1. + virtual void storeEquipmentState (const MWWorld::LiveCellRefBase& ref, int index, ESM::InventoryState& inventory) const; + virtual void readEquipmentState (const MWWorld::ContainerStoreIterator& iter, int index, const ESM::InventoryState& inventory); public: @@ -141,7 +140,10 @@ namespace MWWorld /// @return if stacking happened, return iterator to the item that was stacked against, otherwise iterator to the newly inserted item. void equip (int slot, const ContainerStoreIterator& iterator, const Ptr& actor); - ///< \note \a iterator can be an end-iterator + ///< \warning \a iterator can not be an end()-iterator, use unequip function instead + + bool isEquipped(const MWWorld::Ptr& item); + ///< Utility function, returns true if the given item is equipped in any slot void setSelectedEnchantItem(const ContainerStoreIterator& iterator); ///< set the selected magic item (for using enchantments of type "Cast once" or "Cast when used") @@ -198,8 +200,15 @@ namespace MWWorld void purgeEffect (short effectId); ///< Remove a magic effect + void purgeEffect (short effectId, const std::string& sourceId); + ///< Remove a magic effect + virtual void clear(); ///< Empty container. + + virtual void writeState (ESM::InventoryState& state); + + virtual void readState (const ESM::InventoryState& state); }; } diff --git a/apps/openmw/mwworld/livecellref.cpp b/apps/openmw/mwworld/livecellref.cpp index 0921d3a1b..bfc708185 100644 --- a/apps/openmw/mwworld/livecellref.cpp +++ b/apps/openmw/mwworld/livecellref.cpp @@ -1,6 +1,7 @@ - #include "livecellref.hpp" +#include + #include #include "../mwbase/environment.hpp" @@ -30,8 +31,18 @@ void MWWorld::LiveCellRefBase::loadImp (const ESM::ObjectState& state) { if (const ESM::Script* script = MWBase::Environment::get().getWorld()->getStore().get().search (scriptId)) { - mData.setLocals (*script); - mData.getLocals().read (state.mLocals, scriptId); + try + { + mData.setLocals (*script); + mData.getLocals().read (state.mLocals, scriptId); + } + catch (const std::exception& exception) + { + std::cerr + << "failed to load state for local script " << scriptId + << " because an exception has been thrown: " << exception.what() + << std::endl; + } } } } diff --git a/apps/openmw/mwworld/localscripts.cpp b/apps/openmw/mwworld/localscripts.cpp index 8a671cea8..5be66ccea 100644 --- a/apps/openmw/mwworld/localscripts.cpp +++ b/apps/openmw/mwworld/localscripts.cpp @@ -1,5 +1,7 @@ #include "localscripts.hpp" +#include + #include "esmstore.hpp" #include "cellstore.hpp" @@ -17,7 +19,7 @@ namespace cellRefList.mList.begin()); iter!=cellRefList.mList.end(); ++iter) { - if (!iter->mBase->mScript.empty() && iter->mData.getCount()) + if (!iter->mBase->mScript.empty() && !iter->mData.isDeleted()) { localScripts.add (iter->mBase->mScript, MWWorld::Ptr (&*iter, cell)); } @@ -93,9 +95,18 @@ void MWWorld::LocalScripts::add (const std::string& scriptName, const Ptr& ptr) { if (const ESM::Script *script = mStore.get().find (scriptName)) { - ptr.getRefData().setLocals (*script); + try + { + ptr.getRefData().setLocals (*script); - mScripts.push_back (std::make_pair (scriptName, ptr)); + mScripts.push_back (std::make_pair (scriptName, ptr)); + } + catch (const std::exception& exception) + { + std::cerr + << "failed to add local script " << scriptName + << " because an exception has been thrown: " << exception.what() << std::endl; + } } } diff --git a/apps/openmw/mwworld/manualref.cpp b/apps/openmw/mwworld/manualref.cpp new file mode 100644 index 000000000..30b4fe353 --- /dev/null +++ b/apps/openmw/mwworld/manualref.cpp @@ -0,0 +1,67 @@ +#include "manualref.hpp" + +#include "esmstore.hpp" +#include "cellstore.hpp" + +namespace +{ + + template + void create(const MWWorld::Store& list, const std::string& name, boost::any& refValue, MWWorld::Ptr& ptrValue) + { + const T* base = list.find(name); + + ESM::CellRef cellRef; + cellRef.mRefNum.unset(); + cellRef.mRefID = name; + cellRef.mScale = 1; + cellRef.mFactionRank = 0; + cellRef.mChargeInt = -1; + cellRef.mGoldValue = 1; + cellRef.mEnchantmentCharge = -1; + cellRef.mTeleport = false; + cellRef.mLockLevel = 0; + cellRef.mReferenceBlocked = 0; + + MWWorld::LiveCellRef ref(cellRef, base); + + refValue = ref; + ptrValue = MWWorld::Ptr(&boost::any_cast&>(refValue), 0); + } +} + +MWWorld::ManualRef::ManualRef(const MWWorld::ESMStore& store, const std::string& name, const int count) +{ + std::string lowerName = Misc::StringUtils::lowerCase(name); + switch (store.find(lowerName)) + { + case ESM::REC_ACTI: create(store.get(), lowerName, mRef, mPtr); break; + case ESM::REC_ALCH: create(store.get(), lowerName, mRef, mPtr); break; + case ESM::REC_APPA: create(store.get(), lowerName, mRef, mPtr); break; + case ESM::REC_ARMO: create(store.get(), lowerName, mRef, mPtr); break; + case ESM::REC_BOOK: create(store.get(), lowerName, mRef, mPtr); break; + case ESM::REC_CLOT: create(store.get(), lowerName, mRef, mPtr); break; + case ESM::REC_CONT: create(store.get(), lowerName, mRef, mPtr); break; + case ESM::REC_CREA: create(store.get(), lowerName, mRef, mPtr); break; + case ESM::REC_DOOR: create(store.get(), lowerName, mRef, mPtr); break; + case ESM::REC_INGR: create(store.get(), lowerName, mRef, mPtr); break; + case ESM::REC_LEVC: create(store.get(), lowerName, mRef, mPtr); break; + case ESM::REC_LEVI: create(store.get(), lowerName, mRef, mPtr); break; + case ESM::REC_LIGH: create(store.get(), lowerName, mRef, mPtr); break; + case ESM::REC_LOCK: create(store.get(), lowerName, mRef, mPtr); break; + case ESM::REC_MISC: create(store.get(), lowerName, mRef, mPtr); break; + case ESM::REC_NPC_: create(store.get(), lowerName, mRef, mPtr); break; + case ESM::REC_PROB: create(store.get(), lowerName, mRef, mPtr); break; + case ESM::REC_REPA: create(store.get(), lowerName, mRef, mPtr); break; + case ESM::REC_STAT: create(store.get(), lowerName, mRef, mPtr); break; + case ESM::REC_WEAP: create(store.get(), lowerName, mRef, mPtr); break; + + case 0: + throw std::logic_error("failed to create manual cell ref for " + lowerName + " (unknown ID)"); + + default: + throw std::logic_error("failed to create manual cell ref for " + lowerName + " (unknown type)"); + } + + mPtr.getRefData().setCount(count); +} \ No newline at end of file diff --git a/apps/openmw/mwworld/manualref.hpp b/apps/openmw/mwworld/manualref.hpp index 0becd7524..2fc599471 100644 --- a/apps/openmw/mwworld/manualref.hpp +++ b/apps/openmw/mwworld/manualref.hpp @@ -3,9 +3,7 @@ #include -#include "esmstore.hpp" #include "ptr.hpp" -#include "cellstore.hpp" namespace MWWorld { @@ -18,67 +16,8 @@ namespace MWWorld ManualRef (const ManualRef&); ManualRef& operator= (const ManualRef&); - template - void create (const MWWorld::Store& list, const std::string& name) - { - const T* base = list.find(name); - - ESM::CellRef cellRef; - cellRef.mRefNum.mIndex = 0; - cellRef.mRefNum.mContentFile = -1; - cellRef.mRefID = name; - cellRef.mScale = 1; - cellRef.mFactionRank = 0; - cellRef.mCharge = -1; - cellRef.mGoldValue = 1; - cellRef.mEnchantmentCharge = -1; - cellRef.mTeleport = false; - cellRef.mLockLevel = 0; - cellRef.mReferenceBlocked = 0; - - LiveCellRef ref(cellRef, base); - - mRef = ref; - mPtr = Ptr (&boost::any_cast&> (mRef), 0); - } - public: - - ManualRef (const MWWorld::ESMStore& store, const std::string& name, const int count=1) - { - std::string lowerName = Misc::StringUtils::lowerCase (name); - switch (store.find (lowerName)) - { - case ESM::REC_ACTI: create (store.get(), lowerName); break; - case ESM::REC_ALCH: create (store.get(), lowerName); break; - case ESM::REC_APPA: create (store.get(), lowerName); break; - case ESM::REC_ARMO: create (store.get(), lowerName); break; - case ESM::REC_BOOK: create (store.get(), lowerName); break; - case ESM::REC_CLOT: create (store.get(), lowerName); break; - case ESM::REC_CONT: create (store.get(), lowerName); break; - case ESM::REC_CREA: create (store.get(), lowerName); break; - case ESM::REC_DOOR: create (store.get(), lowerName); break; - case ESM::REC_INGR: create (store.get(), lowerName); break; - case ESM::REC_LEVC: create (store.get(), lowerName); break; - case ESM::REC_LEVI: create (store.get(), lowerName); break; - case ESM::REC_LIGH: create (store.get(), lowerName); break; - case ESM::REC_LOCK: create (store.get(), lowerName); break; - case ESM::REC_MISC: create (store.get(), lowerName); break; - case ESM::REC_NPC_: create (store.get(), lowerName); break; - case ESM::REC_PROB: create (store.get(), lowerName); break; - case ESM::REC_REPA: create (store.get(), lowerName); break; - case ESM::REC_STAT: create (store.get(), lowerName); break; - case ESM::REC_WEAP: create (store.get(), lowerName); break; - - case 0: - throw std::logic_error ("failed to create manual cell ref for " + lowerName + " (unknown ID)"); - - default: - throw std::logic_error ("failed to create manual cell ref for " + lowerName + " (unknown type)"); - } - - mPtr.getRefData().setCount(count); - } + ManualRef(const MWWorld::ESMStore& store, const std::string& name, const int count = 1); const Ptr& getPtr() const { diff --git a/apps/openmw/mwworld/omwloader.cpp b/apps/openmw/mwworld/omwloader.cpp deleted file mode 100644 index 8562a4fe0..000000000 --- a/apps/openmw/mwworld/omwloader.cpp +++ /dev/null @@ -1,17 +0,0 @@ -#include "omwloader.hpp" - -namespace MWWorld -{ - -OmwLoader::OmwLoader(Loading::Listener& listener) - : ContentLoader(listener) -{ -} - -void OmwLoader::load(const boost::filesystem::path& filepath, int& index) -{ - ContentLoader::load(filepath.filename(), index); -} - -} /* namespace MWWorld */ - diff --git a/apps/openmw/mwworld/omwloader.hpp b/apps/openmw/mwworld/omwloader.hpp deleted file mode 100644 index cb9faa430..000000000 --- a/apps/openmw/mwworld/omwloader.hpp +++ /dev/null @@ -1,21 +0,0 @@ -#ifndef OMWLOADER_HPP -#define OMWLOADER_HPP - -#include "contentloader.hpp" - -namespace MWWorld -{ - -/** - * @brief Placeholder for real OpenMW content loader - */ -struct OmwLoader : public ContentLoader -{ - OmwLoader(Loading::Listener& listener); - - void load(const boost::filesystem::path& filepath, int& index); -}; - -} /* namespace MWWorld */ - -#endif /* OMWLOADER_HPP */ diff --git a/apps/openmw/mwworld/physicssystem.cpp b/apps/openmw/mwworld/physicssystem.cpp index daad5b0e6..d31ae520b 100644 --- a/apps/openmw/mwworld/physicssystem.cpp +++ b/apps/openmw/mwworld/physicssystem.cpp @@ -14,8 +14,11 @@ #include #include #include +#include #include +#include +#include #include @@ -23,6 +26,7 @@ #include "../mwbase/environment.hpp" #include "../mwmechanics/creaturestats.hpp" +#include "../mwmechanics/movement.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/cellstore.hpp" @@ -39,7 +43,7 @@ using namespace Ogre; namespace { -void animateCollisionShapes (std::map& map) +void animateCollisionShapes (std::map& map, btDynamicsWorld* dynamicsWorld) { for (std::map::iterator it = map.begin(); it != map.end(); ++it) @@ -49,35 +53,37 @@ void animateCollisionShapes (std::mapgetAnimation(ptr); - if (!animation) // Shouldn't happen either, since keyframe-controlled objects are not batched in StaticGeometry - throw std::runtime_error("can't find Animation for " + ptr.getCellRef().getRefId()); + if (!animation) + continue; OEngine::Physic::AnimatedShapeInstance& instance = it->second; - std::map& shapes = instance.mAnimatedShapes; - for (std::map::iterator shapeIt = shapes.begin(); + std::map& shapes = instance.mAnimatedShapes; + for (std::map::iterator shapeIt = shapes.begin(); shapeIt != shapes.end(); ++shapeIt) { - Ogre::Node* bone; - if (shapeIt->first.empty()) - // HACK: see NifSkeletonLoader::buildBones - bone = animation->getNode(" "); - else - bone = animation->getNode(shapeIt->first); + const std::string& mesh = animation->getObjectRootName(); + int boneHandle = NifOgre::NIFSkeletonLoader::lookupOgreBoneHandle(mesh, shapeIt->first); + Ogre::Node* bone = animation->getNode(boneHandle); if (bone == NULL) - throw std::runtime_error("can't find bone"); + continue; - btCompoundShape* compound = dynamic_cast(instance.mCompound); + btCompoundShape* compound = static_cast(instance.mCompound); btTransform trans; - trans.setOrigin(BtOgre::Convert::toBullet(bone->_getDerivedPosition())); + trans.setOrigin(BtOgre::Convert::toBullet(bone->_getDerivedPosition()) * compound->getLocalScaling()); trans.setRotation(BtOgre::Convert::toBullet(bone->_getDerivedOrientation())); - compound->getChildShape(shapeIt->second)->setLocalScaling(BtOgre::Convert::toBullet(bone->_getDerivedScale())); + compound->getChildShape(shapeIt->second)->setLocalScaling( + compound->getLocalScaling() * + BtOgre::Convert::toBullet(bone->_getDerivedScale())); compound->updateChildTransform(shapeIt->second, trans); } + + // needed because we used btDynamicsWorld::setForceUpdateAllAabbs(false) + dynamicsWorld->updateSingleAabb(it->first); } } @@ -88,7 +94,9 @@ namespace MWWorld { static const float sMaxSlope = 49.0f; - static const float sStepSize = 32.0f; + static const float sStepSizeUp = 34.0f; + static const float sStepSizeDown = 62.0f; + // Arbitrary number. To prevent infinite loops. They shouldn't happen but it's good to be prepared. static const int sMaxIterations = 8; @@ -101,7 +109,7 @@ namespace MWWorld } static bool stepMove(btCollisionObject *colobj, Ogre::Vector3 &position, - const Ogre::Vector3 &velocity, float &remainingTime, + const Ogre::Vector3 &toMove, float &remainingTime, OEngine::Physic::PhysicEngine *engine) { /* @@ -117,7 +125,7 @@ namespace MWWorld * If not successful return 'false'. May fail for these reasons: * - can't move directly up from current position * - having moved up by between epsilon() and sStepSize, can't move forward - * - having moved forward by between epsilon() and velocity*remainingTime, + * - having moved forward by between epsilon() and toMove, * = moved down between 0 and just under sStepSize but slope was too steep, or * = moved the full sStepSize down (FIXME: this could be a bug) * @@ -126,7 +134,7 @@ namespace MWWorld * Starting position. Obstacle or stairs with height upto sStepSize in front. * * +--+ +--+ |XX - * | | -------> velocity | | +--+XX + * | | -------> toMove | | +--+XX * | | | | |XXXXX * | | +--+ | | +--+XXXXX * | | |XX| | | |XXXXXXXX @@ -150,7 +158,7 @@ namespace MWWorld */ OEngine::Physic::ActorTracer tracer, stepper; - stepper.doTrace(colobj, position, position+Ogre::Vector3(0.0f,0.0f,sStepSize), engine); + stepper.doTrace(colobj, position, position+Ogre::Vector3(0.0f,0.0f,sStepSizeUp), engine); if(stepper.mFraction < std::numeric_limits::epsilon()) return false; // didn't even move the smallest representable amount // (TODO: shouldn't this be larger? Why bother with such a small amount?) @@ -164,16 +172,16 @@ namespace MWWorld * | | * <------------------->| | * +--+ +--+ - * |XX| the moved amount is velocity*remainingTime*tracer.mFraction + * |XX| the moved amount is toMove*tracer.mFraction * +--+ * ============================================== */ - tracer.doTrace(colobj, stepper.mEndPos, stepper.mEndPos + velocity*remainingTime, engine); + tracer.doTrace(colobj, stepper.mEndPos, stepper.mEndPos + toMove, engine); if(tracer.mFraction < std::numeric_limits::epsilon()) return false; // didn't even move the smallest representable amount /* - * Try moving back down sStepSize using stepper. + * Try moving back down sStepSizeDown using stepper. * NOTE: if there is an obstacle below (e.g. stairs), we'll be "stepping up". * Below diagram is the case where we "stepped over" an obstacle in front. * @@ -187,9 +195,12 @@ namespace MWWorld * +--+ +--+ * ============================================== */ - stepper.doTrace(colobj, tracer.mEndPos, tracer.mEndPos-Ogre::Vector3(0.0f,0.0f,sStepSize), engine); + stepper.doTrace(colobj, tracer.mEndPos, tracer.mEndPos-Ogre::Vector3(0.0f,0.0f,sStepSizeDown), engine); if(stepper.mFraction < 1.0f && getSlope(stepper.mPlaneNormal) <= sMaxSlope) { + // don't allow stepping up other actors + if (stepper.mHitObject->getBroadphaseHandle()->m_collisionFilterGroup == OEngine::Physic::CollisionType_Actor) + return false; // only step down onto semi-horizontal surfaces. don't step down onto the side of a house or a wall. // TODO: stepper.mPlaneNormal does not appear to be reliable - needs more testing // NOTE: caller's variables 'position' & 'remainingTime' are modified here @@ -234,26 +245,54 @@ namespace MWWorld physicActor->setOnGround(false); return position; } + else + { + // Check if we actually found a valid spawn point (use an infinitely thin ray this time). + // Required for some broken door destinations in Morrowind.esm, where the spawn point + // intersects with other geometry if the actor's base is taken into account + btVector3 from = BtOgre::Convert::toBullet(position); + btVector3 to = from - btVector3(0,0,maxHeight); + + btCollisionWorld::ClosestRayResultCallback resultCallback1(from, to); + resultCallback1.m_collisionFilterGroup = 0xff; + resultCallback1.m_collisionFilterMask = OEngine::Physic::CollisionType_World|OEngine::Physic::CollisionType_HeightMap; + + engine->mDynamicsWorld->rayTest(from, to, resultCallback1); + if (resultCallback1.hasHit() && + (BtOgre::Convert::toOgre(resultCallback1.m_hitPointWorld).distance(tracer.mEndPos) > 30 + || getSlope(tracer.mPlaneNormal) > sMaxSlope)) + { + physicActor->setOnGround(getSlope(BtOgre::Convert::toOgre(resultCallback1.m_hitNormalWorld)) <= sMaxSlope); + return BtOgre::Convert::toOgre(resultCallback1.m_hitPointWorld) + Ogre::Vector3(0,0,1.f); + } - physicActor->setOnGround(getSlope(tracer.mPlaneNormal) <= sMaxSlope); + physicActor->setOnGround(getSlope(tracer.mPlaneNormal) <= sMaxSlope); - return tracer.mEndPos; + return tracer.mEndPos; + } } static Ogre::Vector3 move(const MWWorld::Ptr &ptr, const Ogre::Vector3 &movement, float time, - bool isFlying, float waterlevel, float slowFall, OEngine::Physic::PhysicEngine *engine) + bool isFlying, float waterlevel, float slowFall, OEngine::Physic::PhysicEngine *engine + , std::map& collisionTracker + , std::map& standingCollisionTracker) { const ESM::Position &refpos = ptr.getRefData().getPosition(); Ogre::Vector3 position(refpos.pos); // Early-out for totally static creatures // (Not sure if gravity should still apply?) - if (!ptr.getClass().canWalk(ptr) && !ptr.getClass().canFly(ptr) && !ptr.getClass().canSwim(ptr)) + if (!ptr.getClass().isMobile(ptr)) return position; - /* Anything to collide with? */ OEngine::Physic::PhysicActor *physicActor = engine->getCharacter(ptr.getRefData().getHandle()); - if(!physicActor || !physicActor->getCollisionMode()) + if (!physicActor) + return position; + + // Reset per-frame data + physicActor->setWalkingOnWater(false); + // Anything to collide with? + if(!physicActor->getCollisionMode()) { return position + (Ogre::Quaternion(Ogre::Radian(refpos.rot[2]), Ogre::Vector3::NEGATIVE_UNIT_Z) * Ogre::Quaternion(Ogre::Radian(refpos.rot[0]), Ogre::Vector3::NEGATIVE_UNIT_X)) @@ -264,66 +303,31 @@ namespace MWWorld Ogre::Vector3 halfExtents = physicActor->getHalfExtents(); position.z += halfExtents.z; - waterlevel -= halfExtents.z * 0.5; - /* - * A 3/4 submerged example - * - * +---+ - * | | - * | | <- (original waterlevel) - * | | - * | | <- position <- waterlevel - * | | - * | | - * | | - * +---+ <- (original position) - */ + static const float fSwimHeightScale = MWBase::Environment::get().getWorld()->getStore().get() + .find("fSwimHeightScale")->getFloat(); + float swimlevel = waterlevel + halfExtents.z - (halfExtents.z * 2 * fSwimHeightScale); OEngine::Physic::ActorTracer tracer; - bool wasOnGround = false; - bool isOnGround = false; - Ogre::Vector3 inertia(0.0f); + Ogre::Vector3 inertia = physicActor->getInertialForce(); Ogre::Vector3 velocity; - if(position.z < waterlevel || isFlying) // under water by 3/4 or can fly + if(position.z < swimlevel || isFlying) { - // TODO: Shouldn't water have higher drag in calculating velocity? velocity = (Ogre::Quaternion(Ogre::Radian(refpos.rot[2]), Ogre::Vector3::NEGATIVE_UNIT_Z)* Ogre::Quaternion(Ogre::Radian(refpos.rot[0]), Ogre::Vector3::NEGATIVE_UNIT_X)) * movement; } else { velocity = Ogre::Quaternion(Ogre::Radian(refpos.rot[2]), Ogre::Vector3::NEGATIVE_UNIT_Z) * movement; - // not in water nor can fly, so need to deal with gravity - if(!physicActor->getOnGround()) // if current OnGround status is false, must be falling or jumping - { - // If falling, add part of the incoming velocity with the current inertia - // TODO: but we could be jumping up? - velocity = velocity * time + physicActor->getInertialForce(); - - // avoid getting infinite inertia in air - float actorSpeed = ptr.getClass().getSpeed(ptr); - float speedXY = Ogre::Vector2(velocity.x, velocity.y).length(); - if (speedXY > actorSpeed) - { - velocity.x *= actorSpeed / speedXY; - velocity.y *= actorSpeed / speedXY; - } - } - inertia = velocity; // NOTE: velocity is for z axis only in this code block - if(!(movement.z > 0.0f)) // falling or moving horizontally (or stationary?) check if we're on ground now + if (velocity.z > 0.f) + inertia = velocity; + if(!physicActor->getOnGround()) { - wasOnGround = physicActor->getOnGround(); // store current state - tracer.doTrace(colobj, position, position - Ogre::Vector3(0,0,2), engine); // check if down 2 possible - if(tracer.mFraction < 1.0f && getSlope(tracer.mPlaneNormal) <= sMaxSlope) - { - isOnGround = true; - // if we're on the ground, don't try to fall any more - velocity.z = std::max(0.0f, velocity.z); - } + velocity = velocity + physicActor->getInertialForce(); } } + ptr.getClass().getMovementSettings(ptr).mPosition[2] = 0; // Now that we have the effective movement vector, apply wind forces to it if (MWBase::Environment::get().getWorld()->isInStorm()) @@ -335,6 +339,8 @@ namespace MWWorld velocity *= 1.f-(fStromWalkMult * (angle.valueDegrees()/180.f)); } + Ogre::Vector3 origVelocity = velocity; + Ogre::Vector3 newPosition = position; /* * A loop to find newPosition using tracer, if successful different from the starting position. @@ -344,16 +350,13 @@ namespace MWWorld float remainingTime = time; for(int iterations = 0; iterations < sMaxIterations && remainingTime > 0.01f; ++iterations) { - // NOTE: velocity is either z axis only or x & z axis Ogre::Vector3 nextpos = newPosition + velocity * remainingTime; // If not able to fly, don't allow to swim up into the air - // TODO: this if condition may not work for large creatures or situations - // where the creature gets above the waterline for some reason - if(newPosition.z < waterlevel && // started 3/4 under water + if(newPosition.z < swimlevel && !isFlying && // can't fly - nextpos.z > waterlevel && // but about to go above water - newPosition.z <= waterlevel) + nextpos.z > swimlevel && // but about to go above water + newPosition.z <= swimlevel) { const Ogre::Vector3 down(0,0,-1); Ogre::Real movelen = velocity.normalise(); @@ -373,9 +376,16 @@ namespace MWWorld if(tracer.mFraction >= 1.0f) { newPosition = tracer.mEndPos; // ok to move, so set newPosition - remainingTime *= (1.0f-tracer.mFraction); // FIXME: remainingTime is no longer used so don't set it? break; } + else + { + const btCollisionObject* standingOn = tracer.mHitObject; + if (const OEngine::Physic::RigidBody* body = dynamic_cast(standingOn)) + { + collisionTracker[ptr.getRefData().getHandle()] = body->mName; + } + } } else { @@ -386,7 +396,6 @@ namespace MWWorld // precision can be lost due to any math Bullet does internally). Since we // aren't performing any collision detection, we want to reject the next // position, so that we don't slowly move inside another object. - remainingTime *= (1.0f-tracer.mFraction); // FIXME: remainingTime is no longer used so don't set it? break; } @@ -394,62 +403,103 @@ namespace MWWorld Ogre::Vector3 oldPosition = newPosition; // We hit something. Try to step up onto it. (NOTE: stepMove does not allow stepping over) // NOTE: stepMove modifies newPosition if successful - if(stepMove(colobj, newPosition, velocity, remainingTime, engine)) + bool result = stepMove(colobj, newPosition, velocity*remainingTime, remainingTime, engine); + if (!result) // to make sure the maximum stepping distance isn't framerate-dependent or movement-speed dependent + result = stepMove(colobj, newPosition, velocity.normalisedCopy()*10.f, remainingTime, engine); + if(result) { // don't let pure water creatures move out of water after stepMove - if((ptr.getClass().canSwim(ptr) && !ptr.getClass().canWalk(ptr)) - && newPosition.z > (waterlevel - halfExtents.z * 0.5)) + if (ptr.getClass().isPureWaterCreature(ptr) + && newPosition.z + halfExtents.z > waterlevel) newPosition = oldPosition; - else // Only on the ground if there's gravity - isOnGround = !(newPosition.z < waterlevel || isFlying); } else { // Can't move this way, try to find another spot along the plane - Ogre::Real movelen = velocity.normalise(); + Ogre::Vector3 direction = velocity; + Ogre::Real movelen = direction.normalise(); Ogre::Vector3 reflectdir = velocity.reflect(tracer.mPlaneNormal); reflectdir.normalise(); - velocity = slide(reflectdir, tracer.mPlaneNormal)*movelen; + + Ogre::Vector3 newVelocity = slide(reflectdir, tracer.mPlaneNormal)*movelen; + if ((newVelocity-velocity).squaredLength() < 0.01) + break; + if (velocity.dotProduct(origVelocity) <= 0.f) + break; + + velocity = newVelocity; // Do not allow sliding upward if there is gravity. Stepping will have taken // care of that. - if(!(newPosition.z < waterlevel || isFlying)) + if(!(newPosition.z < swimlevel || isFlying)) velocity.z = std::min(velocity.z, 0.0f); } } - if(isOnGround || wasOnGround) + bool isOnGround = false; + if (!(inertia.z > 0.f) && !(newPosition.z < swimlevel)) { - tracer.doTrace(colobj, newPosition, newPosition - Ogre::Vector3(0,0,sStepSize+2.0f), engine); - if(tracer.mFraction < 1.0f && getSlope(tracer.mPlaneNormal) <= sMaxSlope) + Ogre::Vector3 from = newPosition; + Ogre::Vector3 to = newPosition - (physicActor->getOnGround() ? + Ogre::Vector3(0,0,sStepSizeDown+2.f) : Ogre::Vector3(0,0,2.f)); + tracer.doTrace(colobj, from, to, engine); + if(tracer.mFraction < 1.0f && getSlope(tracer.mPlaneNormal) <= sMaxSlope + && tracer.mHitObject->getBroadphaseHandle()->m_collisionFilterGroup != OEngine::Physic::CollisionType_Actor) { - newPosition.z = tracer.mEndPos.z + 1.0f; + const btCollisionObject* standingOn = tracer.mHitObject; + if (const OEngine::Physic::RigidBody* body = dynamic_cast(standingOn)) + { + standingCollisionTracker[ptr.getRefData().getHandle()] = body->mName; + } + if (standingOn->getBroadphaseHandle()->m_collisionFilterGroup == OEngine::Physic::CollisionType_Water) + physicActor->setWalkingOnWater(true); + + if (!isFlying) + newPosition.z = tracer.mEndPos.z + 1.0f; + isOnGround = true; } else + { + // standing on actors is not allowed (see above). + // in addition to that, apply a sliding effect away from the center of the actor, + // so that we do not stay suspended in air indefinitely. + if (tracer.mFraction < 1.0f && tracer.mHitObject->getBroadphaseHandle()->m_collisionFilterGroup == OEngine::Physic::CollisionType_Actor) + { + if (Ogre::Vector3(velocity.x, velocity.y, 0).squaredLength() < 100.f*100.f) + { + btVector3 aabbMin, aabbMax; + tracer.mHitObject->getCollisionShape()->getAabb(tracer.mHitObject->getWorldTransform(), aabbMin, aabbMax); + btVector3 center = (aabbMin + aabbMax) / 2.f; + inertia = Ogre::Vector3(position.x - center.x(), position.y - center.y(), 0); + inertia.normalise(); + inertia *= 100; + } + } + isOnGround = false; + } } - if(isOnGround || newPosition.z < waterlevel || isFlying) + if(isOnGround || newPosition.z < swimlevel || isFlying) physicActor->setInertialForce(Ogre::Vector3(0.0f)); else { - float diff = time*-627.2f; + inertia.z += time * -627.2f; if (inertia.z < 0) - diff *= slowFall; - inertia.z += diff; + inertia.z *= slowFall; physicActor->setInertialForce(inertia); } physicActor->setOnGround(isOnGround); - newPosition.z -= halfExtents.z; // remove what was added at the beggining + newPosition.z -= halfExtents.z; // remove what was added at the beginning return newPosition; } }; PhysicsSystem::PhysicsSystem(OEngine::Render::OgreRenderer &_rend) : - mRender(_rend), mEngine(0), mTimeAccum(0.0f) + mRender(_rend), mEngine(0), mTimeAccum(0.0f), mWaterEnabled(false), mWaterHeight(0) { // Create physics. shapeLoader is deleted by the physic engine NifBullet::ManualBulletShapeLoader* shapeLoader = new NifBullet::ManualBulletShapeLoader(); @@ -458,7 +508,10 @@ namespace MWWorld PhysicsSystem::~PhysicsSystem() { + if (mWaterCollisionObject.get()) + mEngine->mDynamicsWorld->removeCollisionObject(mWaterCollisionObject.get()); delete mEngine; + delete OEngine::Physic::BulletShapeManager::getSingletonPtr(); } OEngine::Physic::PhysicEngine* PhysicsSystem::getEngine() @@ -597,9 +650,9 @@ namespace MWWorld } } - std::vector PhysicsSystem::getCollisions(const Ptr &ptr) + std::vector PhysicsSystem::getCollisions(const Ptr &ptr, int collisionGroup, int collisionMask) { - return mEngine->getCollisions(ptr.getRefData().getBaseNode()->getName()); + return mEngine->getCollisions(ptr.getRefData().getBaseNode()->getName(), collisionGroup, collisionMask); } Ogre::Vector3 PhysicsSystem::traceDown(const MWWorld::Ptr &ptr, float maxHeight) @@ -619,20 +672,18 @@ namespace MWWorld mEngine->removeHeightField(x, y); } - void PhysicsSystem::addObject (const Ptr& ptr, bool placeable) + void PhysicsSystem::addObject (const Ptr& ptr, const std::string& mesh, bool placeable) { - std::string mesh = ptr.getClass().getModel(ptr); Ogre::SceneNode* node = ptr.getRefData().getBaseNode(); handleToMesh[node->getName()] = mesh; mEngine->createAndAdjustRigidBody( - mesh, node->getName(), node->getScale().x, node->getPosition(), node->getOrientation(), 0, 0, false, placeable); + mesh, node->getName(), ptr.getCellRef().getScale(), node->getPosition(), node->getOrientation(), 0, 0, false, placeable); mEngine->createAndAdjustRigidBody( - mesh, node->getName(), node->getScale().x, node->getPosition(), node->getOrientation(), 0, 0, true, placeable); + mesh, node->getName(), ptr.getCellRef().getScale(), node->getPosition(), node->getOrientation(), 0, 0, true, placeable); } - void PhysicsSystem::addActor (const Ptr& ptr) + void PhysicsSystem::addActor (const Ptr& ptr, const std::string& mesh) { - std::string mesh = ptr.getClass().getModel(ptr); Ogre::SceneNode* node = ptr.getRefData().getBaseNode(); //TODO:optimize this. Searching the std::map isn't very efficient i think. mEngine->addCharacter(node->getName(), mesh, node->getPosition(), node->getScale().x, node->getOrientation()); @@ -652,11 +703,18 @@ namespace MWWorld const Ogre::Vector3 &position = node->getPosition(); if(OEngine::Physic::RigidBody *body = mEngine->getRigidBody(handle)) + { body->getWorldTransform().setOrigin(btVector3(position.x,position.y,position.z)); + mEngine->mDynamicsWorld->updateSingleAabb(body); + } if(OEngine::Physic::RigidBody *body = mEngine->getRigidBody(handle, true)) + { body->getWorldTransform().setOrigin(btVector3(position.x,position.y,position.z)); + mEngine->mDynamicsWorld->updateSingleAabb(body); + } + // Actors update their AABBs every frame (DISABLE_DEACTIVATION), so no need to do it manually if(OEngine::Physic::PhysicActor *physact = mEngine->getCharacter(handle)) physact->setPosition(position); } @@ -678,6 +736,7 @@ namespace MWWorld body->getWorldTransform().setRotation(btQuaternion(rotation.x, rotation.y, rotation.z, rotation.w)); else mEngine->boxAdjustExternal(handleToMesh[handle], body, node->getScale().x, node->getPosition(), rotation); + mEngine->mDynamicsWorld->updateSingleAabb(body); } if (OEngine::Physic::RigidBody* body = mEngine->getRigidBody(handle, true)) { @@ -685,6 +744,7 @@ namespace MWWorld body->getWorldTransform().setRotation(btQuaternion(rotation.x, rotation.y, rotation.z, rotation.w)); else mEngine->boxAdjustExternal(handleToMesh[handle], body, node->getScale().x, node->getPosition(), rotation); + mEngine->mDynamicsWorld->updateSingleAabb(body); } } @@ -694,17 +754,26 @@ namespace MWWorld const std::string &handle = node->getName(); if(handleToMesh.find(handle) != handleToMesh.end()) { + std::string model = ptr.getClass().getModel(ptr); + model = Misc::ResourceHelpers::correctActorModelPath(model); // FIXME: scaling shouldn't require model + bool placeable = false; if (OEngine::Physic::RigidBody* body = mEngine->getRigidBody(handle,true)) placeable = body->mPlaceable; else if (OEngine::Physic::RigidBody* body = mEngine->getRigidBody(handle,false)) placeable = body->mPlaceable; removeObject(handle); - addObject(ptr, placeable); + addObject(ptr, model, placeable); } if (OEngine::Physic::PhysicActor* act = mEngine->getCharacter(handle)) - act->setScale(node->getScale().x); + { + float scale = ptr.getCellRef().getScale(); + if (!ptr.getClass().isNpc()) + // NOTE: Ignoring Npc::adjustScale (race height) on purpose. This is a bug in MW and must be replicated for compatibility reasons + ptr.getClass().adjustScale(ptr, scale); + act->setScale(scale); + } } bool PhysicsSystem::toggleCollisionMode() @@ -735,6 +804,7 @@ namespace MWWorld bool PhysicsSystem::getObjectAABB(const MWWorld::Ptr &ptr, Ogre::Vector3 &min, Ogre::Vector3 &max) { std::string model = ptr.getClass().getModel(ptr); + model = Misc::ResourceHelpers::correctActorModelPath(model); if (model.empty()) { return false; } @@ -769,6 +839,13 @@ namespace MWWorld mMovementQueue.push_back(std::make_pair(ptr, movement)); } + void PhysicsSystem::clearQueuedMovement() + { + mMovementQueue.clear(); + mCollisions.clear(); + mStandingCollisions.clear(); + } + const PtrVelocityList& PhysicsSystem::applyQueuedMovement(float dt) { mMovementResults.clear(); @@ -776,44 +853,41 @@ namespace MWWorld mTimeAccum += dt; if(mTimeAccum >= 1.0f/60.0f) { + // Collision events should be available on every frame + mCollisions.clear(); + mStandingCollisions.clear(); + const MWBase::World *world = MWBase::Environment::get().getWorld(); PtrVelocityList::iterator iter = mMovementQueue.begin(); for(;iter != mMovementQueue.end();++iter) { float waterlevel = -std::numeric_limits::max(); - const ESM::Cell *cell = iter->first.getCell()->getCell(); - if(cell->hasWater()) - waterlevel = cell->mWater; + const MWWorld::CellStore *cell = iter->first.getCell(); + if(cell->getCell()->hasWater()) + waterlevel = cell->getWaterLevel(); float oldHeight = iter->first.getRefData().getPosition().pos[2]; const MWMechanics::MagicEffects& effects = iter->first.getClass().getCreatureStats(iter->first).getMagicEffects(); bool waterCollision = false; - if (effects.get(ESM::MagicEffect::WaterWalking).mMagnitude - && cell->hasWater() + if (effects.get(ESM::MagicEffect::WaterWalking).getMagnitude() + && cell->getCell()->hasWater() && !world->isUnderwater(iter->first.getCell(), Ogre::Vector3(iter->first.getRefData().getPosition().pos))) waterCollision = true; - btStaticPlaneShape planeShape(btVector3(0,0,1), waterlevel); - btCollisionObject object; - object.setCollisionShape(&planeShape); - - // TODO: this seems to have a slight performance impact - if (waterCollision) - mEngine->mDynamicsWorld->addCollisionObject(&object, - 0xff, OEngine::Physic::CollisionType_Actor); + OEngine::Physic::PhysicActor *physicActor = mEngine->getCharacter(iter->first.getRefData().getHandle()); + if (!physicActor) // actor was already removed from the scene + continue; + physicActor->setCanWaterWalk(waterCollision); - // 100 points of slowfall reduce gravity by 90% (this is just a guess) - float slowFall = 1-std::min(std::max(0.f, (effects.get(ESM::MagicEffect::SlowFall).mMagnitude / 100.f) * 0.9f), 0.9f); + // Slow fall reduces fall speed by a factor of (effect magnitude / 200) + float slowFall = 1.f - std::max(0.f, std::min(1.f, effects.get(ESM::MagicEffect::SlowFall).getMagnitude() * 0.005f)); Ogre::Vector3 newpos = MovementSolver::move(iter->first, iter->second, mTimeAccum, world->isFlying(iter->first), - waterlevel, slowFall, mEngine); - - if (waterCollision) - mEngine->mDynamicsWorld->removeCollisionObject(&object); + waterlevel, slowFall, mEngine, mCollisions, mStandingCollisions); float heightDiff = newpos.z - oldHeight; @@ -832,9 +906,106 @@ namespace MWWorld void PhysicsSystem::stepSimulation(float dt) { - animateCollisionShapes(mEngine->mAnimatedShapes); - animateCollisionShapes(mEngine->mAnimatedRaycastingShapes); + animateCollisionShapes(mEngine->mAnimatedShapes, mEngine->mDynamicsWorld); + animateCollisionShapes(mEngine->mAnimatedRaycastingShapes, mEngine->mDynamicsWorld); mEngine->stepSimulation(dt); } + + bool PhysicsSystem::isActorStandingOn(const Ptr &actor, const Ptr &object) const + { + const std::string& actorHandle = actor.getRefData().getHandle(); + const std::string& objectHandle = object.getRefData().getHandle(); + + for (std::map::const_iterator it = mStandingCollisions.begin(); + it != mStandingCollisions.end(); ++it) + { + if (it->first == actorHandle && it->second == objectHandle) + return true; + } + return false; + } + + void PhysicsSystem::getActorsStandingOn(const Ptr &object, std::vector &out) const + { + const std::string& objectHandle = object.getRefData().getHandle(); + + for (std::map::const_iterator it = mStandingCollisions.begin(); + it != mStandingCollisions.end(); ++it) + { + if (it->second == objectHandle) + out.push_back(it->first); + } + } + + bool PhysicsSystem::isActorCollidingWith(const Ptr &actor, const Ptr &object) const + { + const std::string& actorHandle = actor.getRefData().getHandle(); + const std::string& objectHandle = object.getRefData().getHandle(); + + for (std::map::const_iterator it = mCollisions.begin(); + it != mCollisions.end(); ++it) + { + if (it->first == actorHandle && it->second == objectHandle) + return true; + } + return false; + } + + void PhysicsSystem::getActorsCollidingWith(const Ptr &object, std::vector &out) const + { + const std::string& objectHandle = object.getRefData().getHandle(); + + for (std::map::const_iterator it = mCollisions.begin(); + it != mCollisions.end(); ++it) + { + if (it->second == objectHandle) + out.push_back(it->first); + } + } + + void PhysicsSystem::disableWater() + { + if (mWaterEnabled) + { + mWaterEnabled = false; + updateWater(); + } + } + + void PhysicsSystem::enableWater(float height) + { + if (!mWaterEnabled || mWaterHeight != height) + { + mWaterEnabled = true; + mWaterHeight = height; + updateWater(); + } + } + + void PhysicsSystem::setWaterHeight(float height) + { + if (mWaterHeight != height) + { + mWaterHeight = height; + updateWater(); + } + } + + void PhysicsSystem::updateWater() + { + if (mWaterCollisionObject.get()) + { + mEngine->mDynamicsWorld->removeCollisionObject(mWaterCollisionObject.get()); + } + + if (!mWaterEnabled) + return; + + mWaterCollisionObject.reset(new btCollisionObject()); + mWaterCollisionShape.reset(new btStaticPlaneShape(btVector3(0,0,1), mWaterHeight)); + mWaterCollisionObject->setCollisionShape(mWaterCollisionShape.get()); + mEngine->mDynamicsWorld->addCollisionObject(mWaterCollisionObject.get(), OEngine::Physic::CollisionType_Water, + OEngine::Physic::CollisionType_Actor); + } } diff --git a/apps/openmw/mwworld/physicssystem.hpp b/apps/openmw/mwworld/physicssystem.hpp index 8e0be95d5..c1046aacb 100644 --- a/apps/openmw/mwworld/physicssystem.hpp +++ b/apps/openmw/mwworld/physicssystem.hpp @@ -1,6 +1,8 @@ #ifndef GAME_MWWORLD_PHYSICSSYSTEM_H #define GAME_MWWORLD_PHYSICSSYSTEM_H +#include + #include #include @@ -32,9 +34,13 @@ namespace MWWorld PhysicsSystem (OEngine::Render::OgreRenderer &_rend); ~PhysicsSystem (); - void addObject (const MWWorld::Ptr& ptr, bool placeable=false); + void enableWater(float height); + void setWaterHeight(float height); + void disableWater(); + + void addObject (const MWWorld::Ptr& ptr, const std::string& mesh, bool placeable=false); - void addActor (const MWWorld::Ptr& ptr); + void addActor (const MWWorld::Ptr& ptr, const std::string& mesh); void addHeightField (float* heights, int x, int y, float yoffset, @@ -55,7 +61,7 @@ namespace MWWorld void stepSimulation(float dt); - std::vector getCollisions(const MWWorld::Ptr &ptr); ///< get handles this object collides with + std::vector getCollisions(const MWWorld::Ptr &ptr, int collisionGroup, int collisionMask); ///< get handles this object collides with Ogre::Vector3 traceDown(const MWWorld::Ptr &ptr, float maxHeight); std::pair getFacedHandle(float queryDistance); @@ -85,19 +91,53 @@ namespace MWWorld /// be overwritten. Valid until the next call to applyQueuedMovement. void queueObjectMovement(const Ptr &ptr, const Ogre::Vector3 &velocity); + /// Apply all queued movements, then clear the list. const PtrVelocityList& applyQueuedMovement(float dt); + /// Clear the queued movements list without applying. + void clearQueuedMovement(); + + /// Return true if \a actor has been standing on \a object in this frame + /// This will trigger whenever the object is directly below the actor. + /// It doesn't matter if the actor is stationary or moving. + bool isActorStandingOn(const MWWorld::Ptr& actor, const MWWorld::Ptr& object) const; + + /// Get the handle of all actors standing on \a object in this frame. + void getActorsStandingOn(const MWWorld::Ptr& object, std::vector& out) const; + + /// Return true if \a actor has collided with \a object in this frame. + /// This will detect running into objects, but will not detect climbing stairs, stepping up a small object, etc. + bool isActorCollidingWith(const MWWorld::Ptr& actor, const MWWorld::Ptr& object) const; + + /// Get the handle of all actors colliding with \a object in this frame. + void getActorsCollidingWith(const MWWorld::Ptr& object, std::vector& out) const; + private: + void updateWater(); + OEngine::Render::OgreRenderer &mRender; OEngine::Physic::PhysicEngine* mEngine; std::map handleToMesh; + // Tracks all movement collisions happening during a single frame. + // This will detect e.g. running against a vertical wall. It will not detect climbing up stairs, + // stepping up small objects, etc. + std::map mCollisions; + + std::map mStandingCollisions; + PtrVelocityList mMovementQueue; PtrVelocityList mMovementResults; float mTimeAccum; + float mWaterHeight; + float mWaterEnabled; + + std::auto_ptr mWaterCollisionObject; + std::auto_ptr mWaterCollisionShape; + PhysicsSystem (const PhysicsSystem&); PhysicsSystem& operator= (const PhysicsSystem&); }; diff --git a/apps/openmw/mwworld/player.cpp b/apps/openmw/mwworld/player.cpp index 12908ca9d..8f3560a69 100644 --- a/apps/openmw/mwworld/player.cpp +++ b/apps/openmw/mwworld/player.cpp @@ -225,11 +225,9 @@ namespace MWWorld writer.startRecord (ESM::REC_PLAY); player.save (writer); writer.endRecord (ESM::REC_PLAY); - - progress.increaseProgress(); } - bool Player::readRecord (ESM::ESMReader& reader, int32_t type) + bool Player::readRecord (ESM::ESMReader& reader, uint32_t type) { if (type==ESM::REC_PLAY) { @@ -244,6 +242,8 @@ namespace MWWorld mPlayer.load (player.mObject); + getPlayer().getClass().getCreatureStats(getPlayer()).getAiSequence().clear(); + MWBase::World& world = *MWBase::Environment::get().getWorld(); try @@ -253,12 +253,27 @@ namespace MWWorld catch (...) { // Cell no longer exists. Place the player in a default cell. + ESM::Position pos = mPlayer.mData.getPosition(); + MWBase::Environment::get().getWorld()->indexToPosition(0, 0, pos.pos[0], pos.pos[1], true); + pos.pos[2] = 0; + mPlayer.mData.setPosition(pos); mCellStore = world.getExterior(0,0); } - if (!player.mBirthsign.empty() && - !world.getStore().get().search (player.mBirthsign)) - throw std::runtime_error ("invalid player state record (birthsign)"); + if (!player.mBirthsign.empty()) + { + const ESM::BirthSign* sign = world.getStore().get().search (player.mBirthsign); + if (!sign) + throw std::runtime_error ("invalid player state record (birthsign does not exist)"); + + // To handle the case where a birth sign was edited in between play sessions (does not yet handle removing the old spells) + // Also needed for ess-imported savegames which do not specify the birtsign spells in the player's spell list. + for (std::vector::const_iterator iter (sign->mPowers.mList.begin()); + iter!=sign->mPowers.mList.end(); ++iter) + { + getPlayer().getClass().getCreatureStats(getPlayer()).getSpells().add (*iter); + } + } mCurrentCrimeId = player.mCurrentCrimeId; mPaidCrimeId = player.mPaidCrimeId; diff --git a/apps/openmw/mwworld/player.hpp b/apps/openmw/mwworld/player.hpp index d8cde5952..25d8981cd 100644 --- a/apps/openmw/mwworld/player.hpp +++ b/apps/openmw/mwworld/player.hpp @@ -102,7 +102,7 @@ namespace MWWorld void write (ESM::ESMWriter& writer, Loading::Listener& progress) const; - bool readRecord (ESM::ESMReader& reader, int32_t type); + bool readRecord (ESM::ESMReader& reader, uint32_t type); int getNewCrimeId(); // get new id for witnesses void recordCrimeId(); // record the paid crime id when bounty is 0 diff --git a/apps/openmw/mwworld/projectilemanager.cpp b/apps/openmw/mwworld/projectilemanager.cpp index fb376bb93..c97e4e3a5 100644 --- a/apps/openmw/mwworld/projectilemanager.cpp +++ b/apps/openmw/mwworld/projectilemanager.cpp @@ -9,6 +9,7 @@ #include "../mwworld/manualref.hpp" #include "../mwworld/class.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwbase/soundmanager.hpp" @@ -20,6 +21,8 @@ #include "../mwmechanics/spellcasting.hpp" #include "../mwrender/effectmanager.hpp" +#include "../mwrender/animation.hpp" +#include "../mwrender/renderconst.hpp" #include "../mwsound/sound.hpp" @@ -41,6 +44,9 @@ namespace MWWorld if(state.mObject->mControllers[i].getSource().isNull()) state.mObject->mControllers[i].setSource(Ogre::SharedPtr (new MWRender::EffectAnimationTime())); } + + MWRender::Animation::setRenderProperties(state.mObject, MWRender::RV_Effects, + MWRender::RQG_Main, MWRender::RQG_Alpha, 0.f, false, NULL); } void ProjectileManager::update(NifOgre::ObjectScenePtr object, float duration) @@ -148,7 +154,8 @@ namespace MWWorld Ogre::Vector3 pos(it->mNode->getPosition()); Ogre::Vector3 newPos = pos + direction * duration * speed; - it->mSound->setPosition(newPos); + if (it->mSound.get()) + it->mSound->setPosition(newPos); it->mNode->setPosition(newPos); @@ -158,7 +165,8 @@ namespace MWWorld // TODO: use a proper btRigidBody / btGhostObject? btVector3 from(pos.x, pos.y, pos.z); btVector3 to(newPos.x, newPos.y, newPos.z); - std::vector > collisions = mPhysEngine.rayTest2(from, to); + + std::vector > collisions = mPhysEngine.rayTest2(from, to, OEngine::Physic::CollisionType_Projectile); bool hit=false; for (std::vector >::iterator cIt = collisions.begin(); cIt != collisions.end() && !hit; ++cIt) @@ -199,7 +207,7 @@ namespace MWWorld if (hit) { MWWorld::Ptr caster = MWBase::Environment::get().getWorld()->searchPtrViaActorId(it->mActorId); - MWBase::Environment::get().getWorld()->explodeSpell(pos, it->mEffects, caster, it->mSpellId, it->mSourceName); + MWBase::Environment::get().getWorld()->explodeSpell(pos, it->mEffects, caster, ESM::RT_Target, it->mSpellId, it->mSourceName); MWBase::Environment::get().getSoundManager()->stopSound(it->mSound); @@ -234,7 +242,7 @@ namespace MWWorld // TODO: use a proper btRigidBody / btGhostObject? btVector3 from(pos.x, pos.y, pos.z); btVector3 to(newPos.x, newPos.y, newPos.z); - std::vector > collisions = mPhysEngine.rayTest2(from, to); + std::vector > collisions = mPhysEngine.rayTest2(from, to, OEngine::Physic::CollisionType_Projectile); bool hit=false; for (std::vector >::iterator cIt = collisions.begin(); cIt != collisions.end() && !hit; ++cIt) @@ -247,29 +255,23 @@ namespace MWWorld if (obstacle == caster) continue; - if (obstacle.isEmpty()) + MWWorld::ManualRef projectileRef(MWBase::Environment::get().getWorld()->getStore(), it->mId); + + // Try to get a Ptr to the bow that was used. It might no longer exist. + MWWorld::Ptr bow = projectileRef.getPtr(); + if (!caster.isEmpty()) { - // Terrain - } - else if (obstacle.getClass().isActor()) - { - MWWorld::ManualRef projectileRef(MWBase::Environment::get().getWorld()->getStore(), it->mId); - - // Try to get a Ptr to the bow that was used. It might no longer exist. - MWWorld::Ptr bow = projectileRef.getPtr(); - if (!caster.isEmpty()) - { - MWWorld::InventoryStore& inv = caster.getClass().getInventoryStore(caster); - MWWorld::ContainerStoreIterator invIt = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - if (invIt != inv.end() && Misc::StringUtils::ciEqual(invIt->getCellRef().getRefId(), it->mBowId)) - bow = *invIt; - } - - if (caster.isEmpty()) - caster = obstacle; - - MWMechanics::projectileHit(caster, obstacle, bow, projectileRef.getPtr(), pos + (newPos - pos) * cIt->first); + MWWorld::InventoryStore& inv = caster.getClass().getInventoryStore(caster); + MWWorld::ContainerStoreIterator invIt = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); + if (invIt != inv.end() && Misc::StringUtils::ciEqual(invIt->getCellRef().getRefId(), it->mBowId)) + bow = *invIt; } + + if (caster.isEmpty()) + caster = obstacle; + + MWMechanics::projectileHit(caster, obstacle, bow, projectileRef.getPtr(), pos + (newPos - pos) * cIt->first); + hit = true; } if (hit) @@ -317,8 +319,6 @@ namespace MWWorld state.save(writer); writer.endRecord(ESM::REC_PROJ); - - progress.increaseProgress(); } for (std::vector::const_iterator it = mMagicBolts.begin(); it != mMagicBolts.end(); ++it) @@ -341,12 +341,10 @@ namespace MWWorld state.save(writer); writer.endRecord(ESM::REC_MPRJ); - - progress.increaseProgress(); } } - bool ProjectileManager::readRecord(ESM::ESMReader &reader, int32_t type) + bool ProjectileManager::readRecord(ESM::ESMReader &reader, uint32_t type) { if (type == ESM::REC_PROJ) { @@ -409,6 +407,7 @@ namespace MWWorld MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); state.mSound = sndMgr->playManualSound3D(esm.mPosition, esm.mSound, 1.0f, 1.0f, MWBase::SoundManager::Play_TypeSfx, MWBase::SoundManager::Play_Loop); + state.mSoundId = esm.mSound; mMagicBolts.push_back(state); return true; diff --git a/apps/openmw/mwworld/projectilemanager.hpp b/apps/openmw/mwworld/projectilemanager.hpp index 6e84b9efb..93f54c008 100644 --- a/apps/openmw/mwworld/projectilemanager.hpp +++ b/apps/openmw/mwworld/projectilemanager.hpp @@ -53,7 +53,7 @@ namespace MWWorld void clear(); void write (ESM::ESMWriter& writer, Loading::Listener& progress) const; - bool readRecord (ESM::ESMReader& reader, int32_t type); + bool readRecord (ESM::ESMReader& reader, uint32_t type); int countSavedGameRecords() const; private: diff --git a/apps/openmw/mwworld/ptr.hpp b/apps/openmw/mwworld/ptr.hpp index 2f37a1cfd..4d928dacf 100644 --- a/apps/openmw/mwworld/ptr.hpp +++ b/apps/openmw/mwworld/ptr.hpp @@ -6,7 +6,7 @@ #include #include -#include "cellreflist.hpp" +#include "livecellref.hpp" namespace MWWorld { diff --git a/apps/openmw/mwworld/recordcmp.hpp b/apps/openmw/mwworld/recordcmp.hpp index 7de4f5565..500f86b1e 100644 --- a/apps/openmw/mwworld/recordcmp.hpp +++ b/apps/openmw/mwworld/recordcmp.hpp @@ -5,6 +5,8 @@ #include +#include + namespace MWWorld { struct RecordCmp diff --git a/apps/openmw/mwworld/refdata.cpp b/apps/openmw/mwworld/refdata.cpp index f4bc64b70..c2a5e5f83 100644 --- a/apps/openmw/mwworld/refdata.cpp +++ b/apps/openmw/mwworld/refdata.cpp @@ -23,6 +23,7 @@ namespace MWWorld mPosition = refData.mPosition; mLocalRotation = refData.mLocalRotation; mChanged = refData.mChanged; + mDeleted = refData.mDeleted; mCustomData = refData.mCustomData ? refData.mCustomData->clone() : 0; } @@ -36,7 +37,7 @@ namespace MWWorld } RefData::RefData() - : mBaseNode(0), mHasLocals (false), mEnabled (true), mCount (1), mCustomData (0), mChanged(false) + : mBaseNode(0), mHasLocals (false), mEnabled (true), mCount (1), mCustomData (0), mChanged(false), mDeleted(false) { for (int i=0; i<3; ++i) { @@ -49,7 +50,8 @@ namespace MWWorld RefData::RefData (const ESM::CellRef& cellRef) : mBaseNode(0), mHasLocals (false), mEnabled (true), mCount (1), mPosition (cellRef.mPos), mCustomData (0), - mChanged(false) // Loading from ESM/ESP files -> assume unchanged + mChanged(false), // Loading from ESM/ESP files -> assume unchanged + mDeleted(false) { mLocalRotation.rot[0]=0; mLocalRotation.rot[1]=0; @@ -59,7 +61,8 @@ namespace MWWorld RefData::RefData (const ESM::ObjectState& objectState) : mBaseNode (0), mHasLocals (false), mEnabled (objectState.mEnabled), mCount (objectState.mCount), mPosition (objectState.mPosition), mCustomData (0), - mChanged(true) // Loading from a savegame -> assume changed + mChanged(true), // Loading from a savegame -> assume changed + mDeleted(false) { for (int i=0; i<3; ++i) mLocalRotation.rot[i] = objectState.mLocalRotation[i]; @@ -167,6 +170,21 @@ namespace MWWorld mCount = count; } + void RefData::setDeleted(bool deleted) + { + mDeleted = deleted; + } + + bool RefData::isDeleted() const + { + return mDeleted || mCount == 0; + } + + bool RefData::isDeletedByContentFile() const + { + return mDeleted; + } + MWScript::Locals& RefData::getLocals() { return mLocals; diff --git a/apps/openmw/mwworld/refdata.hpp b/apps/openmw/mwworld/refdata.hpp index db66c091b..e90b44f9c 100644 --- a/apps/openmw/mwworld/refdata.hpp +++ b/apps/openmw/mwworld/refdata.hpp @@ -33,10 +33,12 @@ namespace MWWorld MWScript::Locals mLocals; // if we find the overhead of heaving a locals // object in the refdata of refs without a script, // we can make this a pointer later. + bool mDeleted; // separate delete flag used for deletion by a content file bool mHasLocals; bool mEnabled; int mCount; // 0: deleted + ESM::Position mPosition; LocalRotation mLocalRotation; @@ -86,12 +88,21 @@ namespace MWWorld void setLocals (const ESM::Script& script); void setCount (int count); - /// Set object count (an object pile is a simple object with a count >1). + ///< Set object count (an object pile is a simple object with a count >1). /// /// \warning Do not call setCount() to add or remove objects from a /// container or an actor's inventory. Call ContainerStore::add() or /// ContainerStore::remove() instead. + /// This flag is only used for content stack loading and will not be stored in the savegame. + /// If the object was deleted by gameplay, then use setCount(0) instead. + void setDeleted(bool deleted); + + /// Returns true if the object was either deleted by the content file or by gameplay. + bool isDeleted() const; + /// Returns true if the object was deleted by a content file. + bool isDeletedByContentFile() const; + MWScript::Locals& getLocals(); bool isEnabled() const; diff --git a/apps/openmw/mwworld/scene.cpp b/apps/openmw/mwworld/scene.cpp index b2faa1a01..8d689240b 100644 --- a/apps/openmw/mwworld/scene.cpp +++ b/apps/openmw/mwworld/scene.cpp @@ -3,11 +3,10 @@ #include #include - -#include +#include #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" /// FIXME +#include "../mwbase/world.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" @@ -22,6 +21,18 @@ namespace { + + void addObject(const MWWorld::Ptr& ptr, MWWorld::PhysicsSystem& physics, + MWRender::RenderingManager& rendering) + { + std::string model = Misc::ResourceHelpers::correctActorModelPath(ptr.getClass().getModel(ptr)); + std::string id = ptr.getClass().getId(ptr); + if (id == "prisonmarker" || id == "divinemarker" || id == "templemarker" || id == "northmarker") + model = ""; // marker objects that have a hardcoded function in the game logic, should be hidden from the player + rendering.addObject(ptr, model); + ptr.getClass().insertObject (ptr, model, physics); + } + void updateObjectLocalRotation (const MWWorld::Ptr& ptr, MWWorld::PhysicsSystem& physics, MWRender::RenderingManager& rendering) { @@ -77,16 +88,19 @@ namespace ptr.getCellRef().setScale(2); } - if (ptr.getRefData().getCount() && ptr.getRefData().isEnabled()) + if (!ptr.getRefData().isDeleted() && ptr.getRefData().isEnabled()) { try { - mRendering.addObject (ptr); - ptr.getClass().insertObject (ptr, mPhysics); - + addObject(ptr, mPhysics, mRendering); updateObjectLocalRotation(ptr, mPhysics, mRendering); - MWBase::Environment::get().getWorld()->scaleObject (ptr, ptr.getCellRef().getScale()); - ptr.getClass().adjustPosition (ptr); + if (ptr.getRefData().getBaseNode()) + { + float scale = ptr.getCellRef().getScale(); + ptr.getClass().adjustScale(ptr, scale); + mRendering.scaleObject(ptr, Ogre::Vector3(scale)); + } + ptr.getClass().adjustPosition (ptr, false); } catch (const std::exception& e) { @@ -119,6 +133,28 @@ namespace MWWorld } } + void Scene::getGridCenter(int &cellX, int &cellY) + { + int maxX = std::numeric_limits::min(); + int maxY = std::numeric_limits::min(); + int minX = std::numeric_limits::max(); + int minY = std::numeric_limits::max(); + CellStoreCollection::iterator iter = mActiveCells.begin(); + while (iter!=mActiveCells.end()) + { + assert ((*iter)->getCell()->isExterior()); + int x = (*iter)->getCell()->getGridX(); + int y = (*iter)->getCell()->getGridY(); + maxX = std::max(x, maxX); + maxY = std::max(y, maxY); + minX = std::min(x, minX); + minY = std::min(y, minY); + ++iter; + } + cellX = (minX + maxX) / 2; + cellY = (minY + maxY) / 2; + } + void Scene::update (float duration, bool paused) { if (mNeedMapUpdate) @@ -128,6 +164,13 @@ namespace MWWorld for (CellStoreCollection::iterator active = mActiveCells.begin(); active!=mActiveCells.end(); ++active) mRendering.requestMap(*active); mNeedMapUpdate = false; + + if (mCurrentCell->isExterior()) + { + int cellX, cellY; + getGridCenter(cellX, cellY); + MWBase::Environment::get().getWindowManager()->setActiveMap(cellX,cellY,false); + } } mRendering.update (duration, paused); @@ -156,7 +199,7 @@ namespace MWWorld (*iter)->getCell()->getGridX(), (*iter)->getCell()->getGridY() ); - if (land) + if (land && land->mDataTypes&ESM::Land::DATA_VHGT) mPhysics->removeHeightField ((*iter)->getCell()->getGridX(), (*iter)->getCell()->getGridY()); } @@ -176,6 +219,8 @@ namespace MWWorld if(result.second) { + std::cout << "loading cell " << cell->getCell()->getDescription() << std::endl; + float verts = ESM::Land::LAND_SIZE; float worldsize = ESM::Land::REAL_SIZE; @@ -187,7 +232,12 @@ namespace MWWorld cell->getCell()->getGridX(), cell->getCell()->getGridY() ); - if (land) { + if (land && land->mDataTypes&ESM::Land::DATA_VHGT) { + // Actually only VHGT is needed here, but we'll need the rest for rendering anyway. + // Load everything now to reduce IO overhead. + const int flags = ESM::Land::DATA_VCLR|ESM::Land::DATA_VHGT|ESM::Land::DATA_VNML|ESM::Land::DATA_VTEX; + if (!land->isDataLoaded(flags)) + land->loadData(flags); mPhysics->addHeightField ( land->mLandData->mHeights, cell->getCell()->getGridX(), @@ -206,6 +256,15 @@ namespace MWWorld insertCell (*cell, true, loadingListener); mRendering.cellAdded (cell); + bool waterEnabled = cell->getCell()->hasWater(); + mRendering.setWaterEnabled(waterEnabled); + if (waterEnabled) + { + mPhysics->enableWater(cell->getWaterLevel()); + mRendering.setWaterHeight(cell->getWaterLevel()); + } + else + mPhysics->disableWater(); mRendering.configureAmbient(*cell); } @@ -215,41 +274,6 @@ namespace MWWorld MWBase::Environment::get().getWorld()->getLocalScripts().addCell (cell); } - void Scene::playerCellChange(CellStore *cell, const ESM::Position& pos, bool adjustPlayerPos) - { - MWBase::World *world = MWBase::Environment::get().getWorld(); - MWWorld::Ptr old = world->getPlayerPtr(); - world->getPlayer().setCell(cell); - - MWWorld::Ptr player = world->getPlayerPtr(); - mRendering.updatePlayerPtr(player); - - if (adjustPlayerPos) { - world->moveObject(player, pos.pos[0], pos.pos[1], pos.pos[2]); - - float x = Ogre::Radian(pos.rot[0]).valueDegrees(); - float y = Ogre::Radian(pos.rot[1]).valueDegrees(); - float z = Ogre::Radian(pos.rot[2]).valueDegrees(); - world->rotateObject(player, x, y, z); - - player.getClass().adjustPosition(player); - } - - MWBase::MechanicsManager *mechMgr = - MWBase::Environment::get().getMechanicsManager(); - - mechMgr->updateCell(old, player); - mechMgr->watchActor(player); - - mRendering.updateTerrain(); - - // Delay the map update until scripts have been given a chance to run. - // If we don't do this, objects that should be disabled will still appear on the map. - mNeedMapUpdate = true; - - MWBase::Environment::get().getWindowManager()->changeCell(mCurrentCell); - } - void Scene::changeToVoid() { CellStoreCollection::iterator active = mActiveCells.begin(); @@ -259,10 +283,29 @@ namespace MWWorld mCurrentCell = NULL; } - void Scene::changeCell (int X, int Y, const ESM::Position& position, bool adjustPlayerPos) + void Scene::playerMoved(const Ogre::Vector3 &pos) { - Nif::NIFFile::CacheLock cachelock; + if (!mCurrentCell || !mCurrentCell->isExterior()) + return; + // figure out the center of the current cell grid (*not* necessarily mCurrentCell, which is the cell the player is in) + int cellX, cellY; + getGridCenter(cellX, cellY); + float centerX, centerY; + MWBase::Environment::get().getWorld()->indexToPosition(cellX, cellY, centerX, centerY, true); + const float maxDistance = 8192/2 + 1024; // 1/2 cell size + threshold + float distance = std::max(std::abs(centerX-pos.x), std::abs(centerY-pos.y)); + if (distance > maxDistance) + { + int newX, newY; + MWBase::Environment::get().getWorld()->positionToIndex(pos.x, pos.y, newX, newY); + changeCellGrid(newX, newY); + mRendering.updateTerrain(); + } + } + + void Scene::changeCellGrid (int X, int Y) + { Loading::Listener* loadingListener = MWBase::Environment::get().getWindowManager()->getLoadingScreen(); Loading::ScopedLoad load(loadingListener); @@ -271,35 +314,17 @@ namespace MWWorld std::string loadingExteriorText = "#{sLoadingMessage3}"; loadingListener->setLabel(loadingExteriorText); - CellStoreCollection::iterator active = mActiveCells.begin(); + const int halfGridSize = Settings::Manager::getInt("exterior grid size", "Cells")/2; - // get the number of cells to unload - int numUnload = 0; - while (active!=mActiveCells.end()) - { - if ((*active)->getCell()->isExterior()) - { - if (std::abs (X-(*active)->getCell()->getGridX())<=1 && - std::abs (Y-(*active)->getCell()->getGridY())<=1) - { - // keep cells within the new 3x3 grid - ++active; - continue; - } - } - ++active; - ++numUnload; - } - - active = mActiveCells.begin(); + CellStoreCollection::iterator active = mActiveCells.begin(); while (active!=mActiveCells.end()) { if ((*active)->getCell()->isExterior()) { - if (std::abs (X-(*active)->getCell()->getGridX())<=1 && - std::abs (Y-(*active)->getCell()->getGridY())<=1) + if (std::abs (X-(*active)->getCell()->getGridX())<=halfGridSize && + std::abs (Y-(*active)->getCell()->getGridY())<=halfGridSize) { - // keep cells within the new 3x3 grid + // keep cells within the new grid ++active; continue; } @@ -309,8 +334,9 @@ namespace MWWorld int refsToLoad = 0; // get the number of refs to load - for (int x=X-1; x<=X+1; ++x) - for (int y=Y-1; y<=Y+1; ++y) + for (int x=X-halfGridSize; x<=X+halfGridSize; ++x) + { + for (int y=Y-halfGridSize; y<=Y+halfGridSize; ++y) { CellStoreCollection::iterator iter = mActiveCells.begin(); @@ -328,12 +354,14 @@ namespace MWWorld if (iter==mActiveCells.end()) refsToLoad += MWBase::Environment::get().getWorld()->getExterior(x, y)->count(); } + } loadingListener->setProgressRange(refsToLoad); // Load cells - for (int x=X-1; x<=X+1; ++x) - for (int y=Y-1; y<=Y+1; ++y) + for (int x=X-halfGridSize; x<=X+halfGridSize; ++x) + { + for (int y=Y-halfGridSize; y<=Y+halfGridSize; ++y) { CellStoreCollection::iterator iter = mActiveCells.begin(); @@ -355,32 +383,47 @@ namespace MWWorld loadCell (cell, loadingListener); } } + } - // find current cell - CellStoreCollection::iterator iter = mActiveCells.begin(); + CellStore* current = MWBase::Environment::get().getWorld()->getExterior(X,Y); + MWBase::Environment::get().getWindowManager()->changeCell(current); - while (iter!=mActiveCells.end()) - { - assert ((*iter)->getCell()->isExterior()); + mCellChanged = true; - if (X==(*iter)->getCell()->getGridX() && - Y==(*iter)->getCell()->getGridY()) - break; + // Delay the map update until scripts have been given a chance to run. + // If we don't do this, objects that should be disabled will still appear on the map. + mNeedMapUpdate = true; + } - ++iter; - } + void Scene::changePlayerCell(CellStore *cell, const ESM::Position &pos, bool adjustPlayerPos) + { + mCurrentCell = cell; - assert (iter!=mActiveCells.end()); + MWBase::World *world = MWBase::Environment::get().getWorld(); + MWWorld::Ptr old = world->getPlayerPtr(); + world->getPlayer().setCell(cell); - mCurrentCell = *iter; + MWWorld::Ptr player = world->getPlayerPtr(); + mRendering.updatePlayerPtr(player); - // adjust player - playerCellChange (mCurrentCell, position, adjustPlayerPos); + if (adjustPlayerPos) { + world->moveObject(player, pos.pos[0], pos.pos[1], pos.pos[2]); - // Sky system - MWBase::Environment::get().getWorld()->adjustSky(); + float x = Ogre::Radian(pos.rot[0]).valueDegrees(); + float y = Ogre::Radian(pos.rot[1]).valueDegrees(); + float z = Ogre::Radian(pos.rot[2]).valueDegrees(); + world->rotateObject(player, x, y, z); - mCellChanged = true; + player.getClass().adjustPosition(player, true); + } + + MWBase::MechanicsManager *mechMgr = + MWBase::Environment::get().getMechanicsManager(); + + mechMgr->updateCell(old, player); + mechMgr->watchActor(player); + + MWBase::Environment::get().getWorld()->adjustSky(); } //We need the ogre renderer and a scene node. @@ -405,21 +448,19 @@ namespace MWWorld void Scene::changeToInteriorCell (const std::string& cellName, const ESM::Position& position) { - Nif::NIFFile::CacheLock lock; - MWBase::Environment::get().getWorld ()->getFader ()->fadeOut(0.5); - - Loading::Listener* loadingListener = MWBase::Environment::get().getWindowManager()->getLoadingScreen(); - Loading::ScopedLoad load(loadingListener); + CellStore *cell = MWBase::Environment::get().getWorld()->getInterior(cellName); + bool loadcell = (mCurrentCell == NULL); + if(!loadcell) + loadcell = *mCurrentCell != *cell; - mRendering.enableTerrain(false); + MWBase::Environment::get().getWindowManager()->fadeScreenOut(0.5); + Loading::Listener* loadingListener = MWBase::Environment::get().getWindowManager()->getLoadingScreen(); std::string loadingInteriorText = "#{sLoadingMessage2}"; loadingListener->setLabel(loadingInteriorText); + Loading::ScopedLoad load(loadingListener); - CellStore *cell = MWBase::Environment::get().getWorld()->getInterior(cellName); - bool loadcell = (mCurrentCell == NULL); - if(!loadcell) - loadcell = *mCurrentCell != *cell; + mRendering.enableTerrain(false); if(!loadcell) { @@ -431,27 +472,16 @@ namespace MWWorld float z = Ogre::Radian(position.rot[2]).valueDegrees(); world->rotateObject(world->getPlayerPtr(), x, y, z); - world->getPlayerPtr().getClass().adjustPosition(world->getPlayerPtr()); - world->getFader()->fadeIn(0.5f); + world->getPlayerPtr().getClass().adjustPosition(world->getPlayerPtr(), true); + MWBase::Environment::get().getWindowManager()->fadeScreenIn(0.5); return; } std::cout << "Changing to interior\n"; - // remove active - CellStoreCollection::iterator active = mActiveCells.begin(); - - // count number of cells to unload - int numUnload = 0; - while (active!=mActiveCells.end()) - { - ++active; - ++numUnload; - } - // unload int current = 0; - active = mActiveCells.begin(); + CellStoreCollection::iterator active = mActiveCells.begin(); while (active!=mActiveCells.end()) { unloadCell (active++); @@ -462,35 +492,38 @@ namespace MWWorld loadingListener->setProgressRange(refsToLoad); // Load cell. - std::cout << "cellName: " << cell->getCell()->mName << std::endl; - - //Loading Interior loading text - loadCell (cell, loadingListener); - mCurrentCell = cell; + changePlayerCell(cell, position, true); // adjust fog mRendering.configureFog(*mCurrentCell); - // adjust player - playerCellChange (mCurrentCell, position); - // Sky system MWBase::Environment::get().getWorld()->adjustSky(); - mCellChanged = true; - MWBase::Environment::get().getWorld ()->getFader ()->fadeIn(0.5); + mCellChanged = true; MWBase::Environment::get().getWindowManager()->fadeScreenIn(0.5); + + MWBase::Environment::get().getWindowManager()->changeCell(mCurrentCell); + + // Delay the map update until scripts have been given a chance to run. + // If we don't do this, objects that should be disabled will still appear on the map. + mNeedMapUpdate = true; } - void Scene::changeToExteriorCell (const ESM::Position& position) + void Scene::changeToExteriorCell (const ESM::Position& position, bool adjustPlayerPos) { int x = 0; int y = 0; MWBase::Environment::get().getWorld()->positionToIndex (position.pos[0], position.pos[1], x, y); - changeCell (x, y, position, true); + changeCellGrid(x, y); + + CellStore* current = MWBase::Environment::get().getWorld()->getExterior(x, y); + changePlayerCell(current, position, adjustPlayerPos); + + mRendering.updateTerrain(); } CellStore* Scene::getCurrentCell () @@ -511,10 +544,16 @@ namespace MWWorld void Scene::addObjectToScene (const Ptr& ptr) { - mRendering.addObject(ptr); - ptr.getClass().insertObject(ptr, *mPhysics); - MWBase::Environment::get().getWorld()->rotateObject(ptr, 0, 0, 0, true); - MWBase::Environment::get().getWorld()->scaleObject(ptr, ptr.getCellRef().getScale()); + try + { + addObject(ptr, *mPhysics, mRendering); + MWBase::Environment::get().getWorld()->rotateObject(ptr, 0, 0, 0, true); + MWBase::Environment::get().getWorld()->scaleObject(ptr, ptr.getCellRef().getScale()); + } + catch (std::exception& e) + { + std::cerr << "error during rendering: " << e.what() << std::endl; + } } void Scene::removeObjectFromScene (const Ptr& ptr) diff --git a/apps/openmw/mwworld/scene.hpp b/apps/openmw/mwworld/scene.hpp index e0eeee187..a9d80bf17 100644 --- a/apps/openmw/mwworld/scene.hpp +++ b/apps/openmw/mwworld/scene.hpp @@ -60,11 +60,13 @@ namespace MWWorld bool mNeedMapUpdate; - void playerCellChange (CellStore *cell, const ESM::Position& position, - bool adjustPlayerPos = true); - void insertCell (CellStore &cell, bool rescale, Loading::Listener* loadingListener); + // Load and unload cells as necessary to create a cell grid with "X" and "Y" in the center + void changeCellGrid (int X, int Y); + + void getGridCenter(int& cellX, int& cellY); + public: Scene (MWRender::RenderingManager& rendering, PhysicsSystem *physics); @@ -75,19 +77,21 @@ namespace MWWorld void loadCell (CellStore *cell, Loading::Listener* loadingListener); - void changeCell (int X, int Y, const ESM::Position& position, bool adjustPlayerPos); + void playerMoved (const Ogre::Vector3& pos); + + void changePlayerCell (CellStore* newCell, const ESM::Position& position, bool adjustPlayerPos); - CellStore* getCurrentCell (); + CellStore *getCurrentCell(); const CellStoreCollection& getActiveCells () const; bool hasCellChanged() const; - ///< Has the player moved to a different cell, since the last frame? + ///< Has the set of active cells changed, since the last frame? void changeToInteriorCell (const std::string& cellName, const ESM::Position& position); ///< Move to interior cell. - void changeToExteriorCell (const ESM::Position& position); + void changeToExteriorCell (const ESM::Position& position, bool adjustPlayerPos); ///< Move to exterior cell. void changeToVoid(); diff --git a/apps/openmw/mwworld/store.cpp b/apps/openmw/mwworld/store.cpp index 7ef06e841..cdcc00b4d 100644 --- a/apps/openmw/mwworld/store.cpp +++ b/apps/openmw/mwworld/store.cpp @@ -1,6 +1,8 @@ #include "store.hpp" #include "esmstore.hpp" +#include + namespace MWWorld { void Store::handleMovedCellRefs(ESM::ESMReader& esm, ESM::Cell* cell) @@ -11,8 +13,7 @@ void Store::handleMovedCellRefs(ESM::ESMReader& esm, ESM::Cell* cell) ESM::MovedCellRef cMRef; cell->getNextMVRF(esm, cMRef); - MWWorld::Store &cStore = const_cast&>(mEsmStore->get()); - ESM::Cell *cellAlt = const_cast(cStore.searchOrCreate(cMRef.mTarget[0], cMRef.mTarget[1])); + ESM::Cell *cellAlt = const_cast(searchOrCreate(cMRef.mTarget[0], cMRef.mTarget[1])); // Get regular moved reference data. Adapted from CellStore::loadRefs. Maybe we can optimize the following // implementation when the oher implementation works as well. @@ -58,6 +59,7 @@ void Store::load(ESM::ESMReader &esm, const std::string &id) // merge new cell into old cell // push the new references on the list of references to manage (saveContext = true) oldcell->mData = cell.mData; + oldcell->mName = cell.mName; // merge name just to be sure (ID will be the same, but case could have been changed) oldcell->loadCell(esm, true); } else { @@ -74,6 +76,7 @@ void Store::load(ESM::ESMReader &esm, const std::string &id) if (oldcell) { // merge new cell into old cell oldcell->mData = cell.mData; + oldcell->mName = cell.mName; oldcell->loadCell(esm, false); // handle moved ref (MVRF) subrecords @@ -116,4 +119,9 @@ void Store::load(ESM::ESMReader &esm, const std::string &id) } } +void Store::load(ESM::ESMReader &esm, const std::string &id) +{ + load(esm, id, esm.getIndex()); +} + } diff --git a/apps/openmw/mwworld/store.hpp b/apps/openmw/mwworld/store.hpp index 9a442387b..a887272c5 100644 --- a/apps/openmw/mwworld/store.hpp +++ b/apps/openmw/mwworld/store.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include @@ -30,7 +31,7 @@ namespace MWWorld virtual void write (ESM::ESMWriter& writer) const {} - virtual void read (ESM::ESMReader& reader) {} + virtual void read (ESM::ESMReader& reader, const std::string& id) {} ///< Read into dynamic storage }; @@ -99,7 +100,9 @@ namespace MWWorld class Store : public StoreBase { std::map mStatic; - std::vector mShared; + std::vector mShared; // Preserves the record order as it came from the content files (this + // is relevant for the spell autocalc code and selection order + // for heads/hairs in the character creation) std::map mDynamic; typedef std::map Dynamic; @@ -137,25 +140,27 @@ namespace MWWorld // setUp needs to be called again after virtual void clearDynamic() { + // remove the dynamic part of mShared + assert(mShared.size() >= mStatic.size()); + mShared.erase(mShared.begin() + mStatic.size(), mShared.end()); mDynamic.clear(); - mShared.clear(); } const T *search(const std::string &id) const { T item; item.mId = Misc::StringUtils::lowerCase(id); + typename Dynamic::const_iterator dit = mDynamic.find(item.mId); + if (dit != mDynamic.end()) { + return &dit->second; + } + typename std::map::const_iterator it = mStatic.find(item.mId); if (it != mStatic.end() && Misc::StringUtils::ciEqual(it->second.mId, id)) { return &(it->second); } - typename Dynamic::const_iterator dit = mDynamic.find(item.mId); - if (dit != mDynamic.end()) { - return &dit->second; - } - return 0; } @@ -203,18 +208,16 @@ namespace MWWorld void load(ESM::ESMReader &esm, const std::string &id) { std::string idLower = Misc::StringUtils::lowerCase(id); - mStatic[idLower] = T(); - mStatic[idLower].mId = idLower; - mStatic[idLower].load(esm); + + std::pair inserted = mStatic.insert(std::make_pair(idLower, T())); + if (inserted.second) + mShared.push_back(&inserted.first->second); + + inserted.first->second.mId = idLower; + inserted.first->second.load(esm); } void setUp() { - mShared.clear(); - mShared.reserve(mStatic.size()); - typename std::map::iterator it = mStatic.begin(); - for (; it != mStatic.end(); ++it) { - mShared.push_back(&(it->second)); - } } iterator begin() const { @@ -302,6 +305,7 @@ namespace MWWorld mDynamic.erase(it); // have to reinit the whole shared part + assert(mShared.size() >= mStatic.size()); mShared.erase(mShared.begin() + mStatic.size(), mShared.end()); for (it = mDynamic.begin(); it != mDynamic.end(); ++it) { mShared.push_back(&it->second); @@ -322,33 +326,18 @@ namespace MWWorld writer.writeHNString ("NAME", iter->second.mId); iter->second.save (writer); writer.endRecord (T::sRecordId); - progress.increaseProgress(); } } - void read (ESM::ESMReader& reader) + void read (ESM::ESMReader& reader, const std::string& id) { T record; - record.mId = reader.getHNString ("NAME"); + record.mId = id; record.load (reader); insert (record); } }; - template <> - inline void Store::clearDynamic() - { - std::map::iterator iter = mDynamic.begin(); - - while (iter!=mDynamic.end()) - if (iter->first=="player") - ++iter; - else - mDynamic.erase (iter++); - - mShared.clear(); - } - template <> inline void Store::load(ESM::ESMReader &esm, const std::string &id) { std::string idLower = Misc::StringUtils::lowerCase(id); @@ -356,10 +345,9 @@ namespace MWWorld std::map::iterator it = mStatic.find(idLower); if (it == mStatic.end()) { it = mStatic.insert( std::make_pair( idLower, ESM::Dialogue() ) ).first; - it->second.mId = id; // don't smash case here, as this line is printed... I think + it->second.mId = id; // don't smash case here, as this line is printed } - //I am not sure is it need to load the dialog from a plugin if it was already loaded from prevois plugins it->second.load(esm); } @@ -368,15 +356,12 @@ namespace MWWorld ESM::Script scpt; scpt.load(esm); Misc::StringUtils::toLower(scpt.mId); - mStatic[scpt.mId] = scpt; - } - template <> - inline void Store::load(ESM::ESMReader &esm, const std::string &id) { - ESM::StartScript s; - s.load(esm); - s.mId = Misc::StringUtils::toLower(s.mScript); - mStatic[s.mId] = s; + std::pair inserted = mStatic.insert(std::make_pair(scpt.mId, scpt)); + if (inserted.second) + mShared.push_back(&inserted.first->second); + else + inserted.first->second = scpt; } template <> @@ -443,9 +428,7 @@ namespace MWWorld ltexl[lt.mIndex] = lt; } - void load(ESM::ESMReader &esm, const std::string &id) { - load(esm, id, esm.getIndex()); - } + void load(ESM::ESMReader &esm, const std::string &id); iterator begin(size_t plugin) const { assert(plugin < mStatic.size()); @@ -559,6 +542,9 @@ namespace MWWorld if (left.first == right.first) return left.second > right.second; + // Exterior cells are listed in descending, row-major order, + // this is a workaround for an ambiguous chargen_plank reference in the vanilla game. + // there is one at -22,16 and one at -2,-9, the latter should be used. return left.first > right.first; } }; @@ -585,13 +571,8 @@ namespace MWWorld void handleMovedCellRefs(ESM::ESMReader& esm, ESM::Cell* cell); public: - ESMStore *mEsmStore; - typedef SharedIterator iterator; - Store() - {} - const ESM::Cell *search(const std::string &id) const { ESM::Cell cell; cell.mName = Misc::StringUtils::lowerCase(id); @@ -629,9 +610,6 @@ namespace MWWorld } const ESM::Cell *searchOrCreate(int x, int y) { - ESM::Cell cell; - cell.mData.mX = x, cell.mData.mY = y; - std::pair key(x, y); DynamicExt::const_iterator it = mExt.find(key); if (it != mExt.end()) { @@ -643,13 +621,15 @@ namespace MWWorld return &dit->second; } - ESM::Cell *newCell = new ESM::Cell; - newCell->mData.mX = x; - newCell->mData.mY = y; - mExt[std::make_pair(x, y)] = *newCell; - delete newCell; - - return &mExt[std::make_pair(x, y)]; + ESM::Cell newCell; + newCell.mData.mX = x; + newCell.mData.mY = y; + newCell.mData.mFlags = ESM::Cell::HasWater; + newCell.mAmbi.mAmbient = 0; + newCell.mAmbi.mSunlight = 0; + newCell.mAmbi.mFog = 0; + newCell.mAmbi.mFogDensity = 0; + return &mExt.insert(std::make_pair(key, newCell)).first->second; } const ESM::Cell *find(const std::string &id) const { @@ -841,169 +821,123 @@ namespace MWWorld template <> class Store : public StoreBase { - public: - typedef std::vector::const_iterator iterator; - private: - std::vector mStatic; + typedef std::map Interior; + typedef std::map, ESM::Pathgrid> Exterior; - std::vector::iterator mIntBegin, mIntEnd, mExtBegin, mExtEnd; + Interior mInt; + Exterior mExt; - struct IntExtOrdering - { - bool operator()(const ESM::Pathgrid &x, const ESM::Pathgrid &y) const { - // interior pathgrids precedes exterior ones (x < y) - if ((x.mData.mX == 0 && x.mData.mY == 0) && - (y.mData.mX != 0 || y.mData.mY != 0)) - { - return true; - } - return false; - } - }; + Store* mCells; - struct ExtCompare + public: + + Store() + : mCells(NULL) { - bool operator()(const ESM::Pathgrid &x, const ESM::Pathgrid &y) const { - if (x.mData.mX == y.mData.mX) { - return x.mData.mY < y.mData.mY; - } - return x.mData.mX < y.mData.mX; - } - }; + } - public: + void setCells(Store& cells) + { + mCells = &cells; + } void load(ESM::ESMReader &esm, const std::string &id) { - mStatic.push_back(ESM::Pathgrid()); - mStatic.back().load(esm); + ESM::Pathgrid pathgrid; + pathgrid.load(esm); + + // Unfortunately the Pathgrid record model does not specify whether the pathgrid belongs to an interior or exterior cell. + // For interior cells, mCell is the cell name, but for exterior cells it is either the cell name or if that doesn't exist, the cell's region name. + // mX and mY will be (0,0) for interior cells, but there is also an exterior cell with the coordinates of (0,0), so that doesn't help. + // Check whether mCell is an interior cell. This isn't perfect, will break if a Region with the same name as an interior cell is created. + // A proper fix should be made for future versions of the file format. + bool interior = mCells->search(pathgrid.mCell) != NULL; + + // Try to overwrite existing record + if (interior) + { + std::pair ret = mInt.insert(std::make_pair(pathgrid.mCell, pathgrid)); + if (!ret.second) + ret.first->second = pathgrid; + } + else + { + std::pair ret = mExt.insert(std::make_pair(std::make_pair(pathgrid.mData.mX, pathgrid.mData.mY), pathgrid)); + if (!ret.second) + ret.first->second = pathgrid; + } } size_t getSize() const { - return mStatic.size(); + return mInt.size() + mExt.size(); } void setUp() { - IntExtOrdering cmp; - std::sort(mStatic.begin(), mStatic.end(), cmp); - - ESM::Pathgrid pg; - pg.mData.mX = pg.mData.mY = 1; - mExtBegin = - std::lower_bound(mStatic.begin(), mStatic.end(), pg, cmp); - mExtEnd = mStatic.end(); - - mIntBegin = mStatic.begin(); - mIntEnd = mExtBegin; - - std::sort(mIntBegin, mIntEnd, RecordCmp()); - std::sort(mExtBegin, mExtEnd, ExtCompare()); } const ESM::Pathgrid *search(int x, int y) const { - ESM::Pathgrid pg; - pg.mData.mX = x; - pg.mData.mY = y; - - iterator it = - std::lower_bound(mExtBegin, mExtEnd, pg, ExtCompare()); - if (it != mExtEnd && it->mData.mX == x && it->mData.mY == y) { - return &(*it); - } - return 0; + Exterior::const_iterator it = mExt.find(std::make_pair(x,y)); + if (it != mExt.end()) + return &(it->second); + return NULL; + } + + const ESM::Pathgrid *search(const std::string& name) const { + Interior::const_iterator it = mInt.find(name); + if (it != mInt.end()) + return &(it->second); + return NULL; } const ESM::Pathgrid *find(int x, int y) const { - const ESM::Pathgrid *ptr = search(x, y); - if (ptr == 0) { + const ESM::Pathgrid* pathgrid = search(x,y); + if (!pathgrid) + { std::ostringstream msg; - msg << "Pathgrid at (" << x << ", " << y << ") not found"; + msg << "Pathgrid in cell '" << x << " " << y << "' not found"; throw std::runtime_error(msg.str()); } - return ptr; + return pathgrid; } - const ESM::Pathgrid *search(const std::string &name) const { - ESM::Pathgrid pg; - pg.mCell = name; - - iterator it = std::lower_bound(mIntBegin, mIntEnd, pg, RecordCmp()); - if (it != mIntEnd && Misc::StringUtils::ciEqual(it->mCell, name)) { - return &(*it); - } - return 0; - } - - const ESM::Pathgrid *find(const std::string &name) const { - const ESM::Pathgrid *ptr = search(name); - if (ptr == 0) { + const ESM::Pathgrid* find(const std::string& name) const { + const ESM::Pathgrid* pathgrid = search(name); + if (!pathgrid) + { std::ostringstream msg; msg << "Pathgrid in cell '" << name << "' not found"; throw std::runtime_error(msg.str()); } - return ptr; + return pathgrid; } const ESM::Pathgrid *search(const ESM::Cell &cell) const { - if (cell.mData.mFlags & ESM::Cell::Interior) { + if (!(cell.mData.mFlags & ESM::Cell::Interior)) + return search(cell.mData.mX, cell.mData.mY); + else return search(cell.mName); - } - return search(cell.mData.mX, cell.mData.mY); } const ESM::Pathgrid *find(const ESM::Cell &cell) const { - if (cell.mData.mFlags & ESM::Cell::Interior) { + if (!(cell.mData.mFlags & ESM::Cell::Interior)) + return find(cell.mData.mX, cell.mData.mY); + else return find(cell.mName); - } - return find(cell.mData.mX, cell.mData.mY); - } - - iterator begin() const { - return mStatic.begin(); - } - - iterator end() const { - return mStatic.end(); - } - - iterator interiorPathsBegin() const { - return mIntBegin; - } - - iterator interiorPathsEnd() const { - return mIntEnd; - } - - iterator exteriorPathsBegin() const { - return mExtBegin; - } - - iterator exteriorPathsEnd() const { - return mExtEnd; } }; template class IndexedStore { - struct Compare - { - bool operator()(const T &x, const T &y) const { - return x.mIndex < y.mIndex; - } - }; protected: - std::vector mStatic; + typedef typename std::map Static; + Static mStatic; public: - typedef typename std::vector::const_iterator iterator; + typedef typename std::map::const_iterator iterator; IndexedStore() {} - IndexedStore(unsigned int size) { - mStatic.reserve(size); - } - iterator begin() const { return mStatic.begin(); } @@ -1012,10 +946,14 @@ namespace MWWorld return mStatic.end(); } - /// \todo refine loading order void load(ESM::ESMReader &esm) { - mStatic.push_back(T()); - mStatic.back().load(esm); + T record; + record.load(esm); + + // Try to overwrite existing record + std::pair ret = mStatic.insert(std::make_pair(record.mIndex, record)); + if (!ret.second) + ret.first->second = record; } int getSize() const { @@ -1023,40 +961,13 @@ namespace MWWorld } void setUp() { - /// \note This method sorts indexed values for further - /// searches. Every loaded item is present in storage, but - /// latest loaded shadows any previous while searching. - /// If memory cost will be too high, it is possible to remove - /// unused values. - - Compare cmp; - - std::stable_sort(mStatic.begin(), mStatic.end(), cmp); - - typename std::vector::iterator first, next; - next = first = mStatic.begin(); - - while (first != mStatic.end() && ++next != mStatic.end()) { - while (next != mStatic.end() && !cmp(*first, *next)) { - ++next; - } - if (first != --next) { - std::swap(*first, *next); - } - first = ++next; - } } const T *search(int index) const { - T item; - item.mIndex = index; - - iterator it = - std::lower_bound(mStatic.begin(), mStatic.end(), item, Compare()); - if (it != mStatic.end() && it->mIndex == index) { - return &(*it); - } - return 0; + typename Static::const_iterator it = mStatic.find(index); + if (it != mStatic.end()) + return &(it->second); + return NULL; } const T *find(int index) const { @@ -1074,18 +985,12 @@ namespace MWWorld struct Store : public IndexedStore { Store() {} - Store(unsigned int size) - : IndexedStore(size) - {} }; template <> struct Store : public IndexedStore { Store() {} - Store(unsigned int size) - : IndexedStore(size) - {} }; template <> @@ -1142,35 +1047,23 @@ namespace MWWorld } }; - - // Specialisation for ESM::Spell to preserve record order as it was in the content files. - // The NPC spell autocalc code heavily depends on this order. - // We could also do this in the base class, but it's usually not a good idea to depend on record order. template<> - inline void Store::clearDynamic() + inline void Store::setUp() { - // remove the dynamic part of mShared - mShared.erase(mShared.begin() + mStatic.size(), mShared.end()); - mDynamic.clear(); - } - - template<> - inline void Store::load(ESM::ESMReader &esm, const std::string &id) { - std::string idLower = Misc::StringUtils::lowerCase(id); - - std::pair inserted = mStatic.insert(std::make_pair(idLower, ESM::Spell())); - if (inserted.second) - mShared.push_back(&mStatic[idLower]); - - inserted.first->second.mId = idLower; - inserted.first->second.load(esm); - } + // DialInfos marked as deleted are kept during the loading phase, so that the linked list + // structure is kept intact for inserting further INFOs. Delete them now that loading is done. + for (Static::iterator it = mStatic.begin(); it != mStatic.end(); ++it) + { + ESM::Dialogue& dial = it->second; + dial.clearDeletedInfos(); + } - template<> - inline void Store::setUp() - { - // remove the dynamic part of mShared - mShared.erase(mShared.begin() + mStatic.size(), mShared.end()); + mShared.clear(); + mShared.reserve(mStatic.size()); + std::map::iterator it = mStatic.begin(); + for (; it != mStatic.end(); ++it) { + mShared.push_back(&(it->second)); + } } } //end namespace diff --git a/apps/openmw/mwworld/weather.cpp b/apps/openmw/mwworld/weather.cpp index fb45cb034..4440ed030 100644 --- a/apps/openmw/mwworld/weather.cpp +++ b/apps/openmw/mwworld/weather.cpp @@ -1,3 +1,6 @@ +#define _USE_MATH_DEFINES +#include + #include "weather.hpp" #include @@ -6,6 +9,8 @@ #include "../mwbase/world.hpp" #include "../mwbase/soundmanager.hpp" +#include "../mwsound/sound.hpp" + #include "../mwrender/renderingmanager.hpp" #include "player.hpp" @@ -73,11 +78,6 @@ Rain Height Max=700 ? Rain Threshold=0.6 ? Max Raindrops=650 ? */ - - size_t offset = weather.mCloudTexture.find(".tga"); - if (offset != std::string::npos) - weather.mCloudTexture.replace(offset, weather.mCloudTexture.length() - offset, ".dds"); - weather.mIsStorm = (name == "ashstorm" || name == "blight"); mWeatherSettings[name] = weather; @@ -157,12 +157,12 @@ WeatherManager::WeatherManager(MWRender::RenderingManager* rendering,MWWorld::Fa setFallbackWeather(foggy,"foggy"); Weather thunderstorm; - thunderstorm.mRainLoopSoundID = "rain heavy"; + thunderstorm.mAmbientLoopSoundID = "rain heavy"; thunderstorm.mRainEffect = "meshes\\raindrop.nif"; setFallbackWeather(thunderstorm,"thunderstorm"); Weather rain; - rain.mRainLoopSoundID = "rain"; + rain.mAmbientLoopSoundID = "rain"; rain.mRainEffect = "meshes\\raindrop.nif"; setFallbackWeather(rain,"rain"); @@ -191,7 +191,7 @@ WeatherManager::WeatherManager(MWRender::RenderingManager* rendering,MWWorld::Fa WeatherManager::~WeatherManager() { - stopSounds(true); + stopSounds(); } void WeatherManager::setWeather(const String& weather, bool instant) @@ -233,6 +233,8 @@ void WeatherManager::setResult(const String& weatherType) mResult.mCloudSpeed = current.mCloudSpeed; mResult.mGlareView = current.mGlareView; mResult.mAmbientLoopSoundID = current.mAmbientLoopSoundID; + mResult.mAmbientSoundVolume = 1.f; + mResult.mEffectFade = 1.f; mResult.mSunColor = current.mSunDiscSunsetColor; mResult.mIsStorm = current.mIsStorm; @@ -346,14 +348,33 @@ void WeatherManager::transition(float factor) mResult.mNight = current.mNight; - mResult.mIsStorm = current.mIsStorm; - mResult.mParticleEffect = current.mParticleEffect; - mResult.mRainEffect = current.mRainEffect; - mResult.mRainSpeed = current.mRainSpeed; - mResult.mRainFrequency = current.mRainFrequency; + if (factor < 0.5) + { + mResult.mIsStorm = current.mIsStorm; + mResult.mParticleEffect = current.mParticleEffect; + mResult.mRainEffect = current.mRainEffect; + mResult.mParticleEffect = current.mParticleEffect; + mResult.mRainSpeed = current.mRainSpeed; + mResult.mRainFrequency = current.mRainFrequency; + mResult.mAmbientSoundVolume = 1-(factor*2); + mResult.mEffectFade = mResult.mAmbientSoundVolume; + mResult.mAmbientLoopSoundID = current.mAmbientLoopSoundID; + } + else + { + mResult.mIsStorm = other.mIsStorm; + mResult.mParticleEffect = other.mParticleEffect; + mResult.mRainEffect = other.mRainEffect; + mResult.mParticleEffect = other.mParticleEffect; + mResult.mRainSpeed = other.mRainSpeed; + mResult.mRainFrequency = other.mRainFrequency; + mResult.mAmbientSoundVolume = 2*(factor-0.5); + mResult.mEffectFade = mResult.mAmbientSoundVolume; + mResult.mAmbientLoopSoundID = other.mAmbientLoopSoundID; + } } -void WeatherManager::update(float duration) +void WeatherManager::update(float duration, bool paused) { float timePassed = mTimePassed; mTimePassed = 0; @@ -366,7 +387,7 @@ void WeatherManager::update(float duration) { mRendering->skyDisable(); mRendering->getSkyManager()->setLightningStrength(0.f); - stopSounds(true); + stopSounds(); return; } @@ -409,29 +430,35 @@ void WeatherManager::update(float duration) else mRendering->getSkyManager()->sunEnable(); - // sun angle - float height; - - //Day duration - float dayDuration = (mNightStart - 1) - mSunriseTime; - - // rise at 6, set at 20 - if (mHour >= mSunriseTime && mHour <= mNightStart) - height = 1 - std::abs(((mHour - dayDuration) / 7.f)); - else if (mHour > mNightStart) - height = (mHour - mNightStart) / 4.f; - else //if (mHour > 0 && mHour < 6) - height = 1 - (mHour / mSunriseTime); - - int facing = (mHour > 13.f) ? 1 : -1; - - bool sun_is_moon = mHour >= mNightStart || mHour <= mSunriseTime; + // Update the sun direction. Run it east to west at a fixed angle from overhead. + // The sun's speed at day and night may differ, since mSunriseTime and mNightStart + // mark when the sun is level with the horizon. + { + // Shift times into a 24-hour window beginning at mSunriseTime... + float adjustedHour = mHour; + float adjustedNightStart = mNightStart; + if ( mHour < mSunriseTime ) + adjustedHour += 24.f; + if ( mNightStart < mSunriseTime ) + adjustedNightStart += 24.f; + + const bool is_night = adjustedHour >= adjustedNightStart; + const float dayDuration = adjustedNightStart - mSunriseTime; + const float nightDuration = 24.f - dayDuration; + + double theta; + if ( !is_night ) { + theta = M_PI * (adjustedHour - mSunriseTime) / dayDuration; + } else { + theta = M_PI * (adjustedHour - adjustedNightStart) / nightDuration; + } - Vector3 final( - (height - 1) * facing, - (height - 1) * facing, - height); - mRendering->setSunDirection(final, sun_is_moon); + Vector3 final( + cos( theta ), + -0.268f, // approx tan( -15 degrees ) + sin( theta ) ); + mRendering->setSunDirection( final, is_night ); + } /* * TODO: import separated fadeInStart/Finish, fadeOutStart/Finish @@ -488,51 +515,56 @@ void WeatherManager::update(float duration) mRendering->getSkyManager()->secundaDisable(); } - if (mCurrentWeather == "thunderstorm" && mNextWeather == "") + if (!paused) { - if (mThunderFlash > 0) + if (mCurrentWeather == "thunderstorm" && mNextWeather == "") { - // play the sound after a delay - mThunderSoundDelay -= duration; - if (mThunderSoundDelay <= 0) - { - // pick a random sound - int sound = rand() % 4; - std::string* soundName = NULL; - if (sound == 0) soundName = &mThunderSoundID0; - else if (sound == 1) soundName = &mThunderSoundID1; - else if (sound == 2) soundName = &mThunderSoundID2; - else if (sound == 3) soundName = &mThunderSoundID3; - MWBase::Environment::get().getSoundManager()->playSound(*soundName, 1.0, 1.0); - mThunderSoundDelay = 1000; - } - - mThunderFlash -= duration; if (mThunderFlash > 0) - mRendering->getSkyManager()->setLightningStrength( mThunderFlash / mThunderThreshold ); - else { - mThunderChanceNeeded = rand() % 100; - mThunderChance = 0; - mRendering->getSkyManager()->setLightningStrength( 0.f ); + // play the sound after a delay + mThunderSoundDelay -= duration; + if (mThunderSoundDelay <= 0) + { + // pick a random sound + int sound = rand() % 4; + std::string* soundName = NULL; + if (sound == 0) soundName = &mThunderSoundID0; + else if (sound == 1) soundName = &mThunderSoundID1; + else if (sound == 2) soundName = &mThunderSoundID2; + else if (sound == 3) soundName = &mThunderSoundID3; + if (soundName) + MWBase::Environment::get().getSoundManager()->playSound(*soundName, 1.0, 1.0); + mThunderSoundDelay = 1000; + } + + mThunderFlash -= duration; + if (mThunderFlash > 0) + mRendering->getSkyManager()->setLightningStrength( mThunderFlash / mThunderThreshold ); + else + { + mThunderChanceNeeded = rand() % 100; + mThunderChance = 0; + mRendering->getSkyManager()->setLightningStrength( 0.f ); + } } - } - else - { - // no thunder active - mThunderChance += duration*4; // chance increases by 4 percent every second - if (mThunderChance >= mThunderChanceNeeded) + else { - mThunderFlash = mThunderThreshold; + // no thunder active + mThunderChance += duration*4; // chance increases by 4 percent every second + if (mThunderChance >= mThunderChanceNeeded) + { + mThunderFlash = mThunderThreshold; - mRendering->getSkyManager()->setLightningStrength( mThunderFlash / mThunderThreshold ); + mRendering->getSkyManager()->setLightningStrength( mThunderFlash / mThunderThreshold ); - mThunderSoundDelay = 0.25; + mThunderSoundDelay = 0.25; + } } } + else + mRendering->getSkyManager()->setLightningStrength(0.f); } - else - mRendering->getSkyManager()->setLightningStrength(0.f); + mRendering->setAmbientColour(mResult.mAmbientColor); mRendering->sunEnable(false); @@ -541,40 +573,25 @@ void WeatherManager::update(float duration) mRendering->getSkyManager()->setWeather(mResult); // Play sounds - if (mNextWeather == "") + if (mPlayingSoundID != mResult.mAmbientLoopSoundID) { - std::string ambientSnd = mWeatherSettings[mCurrentWeather].mAmbientLoopSoundID; - if (!ambientSnd.empty() && std::find(mSoundsPlaying.begin(), mSoundsPlaying.end(), ambientSnd) == mSoundsPlaying.end()) - { - mSoundsPlaying.push_back(ambientSnd); - MWBase::Environment::get().getSoundManager()->playSound(ambientSnd, 1.0, 1.0, MWBase::SoundManager::Play_TypeSfx, MWBase::SoundManager::Play_Loop); - } + stopSounds(); + if (!mResult.mAmbientLoopSoundID.empty()) + mAmbientSound = MWBase::Environment::get().getSoundManager()->playSound(mResult.mAmbientLoopSoundID, 1.0, 1.0, MWBase::SoundManager::Play_TypeSfx, MWBase::SoundManager::Play_Loop); - std::string rainSnd = mWeatherSettings[mCurrentWeather].mRainLoopSoundID; - if (!rainSnd.empty() && std::find(mSoundsPlaying.begin(), mSoundsPlaying.end(), rainSnd) == mSoundsPlaying.end()) - { - mSoundsPlaying.push_back(rainSnd); - MWBase::Environment::get().getSoundManager()->playSound(rainSnd, 1.0, 1.0, MWBase::SoundManager::Play_TypeSfx, MWBase::SoundManager::Play_Loop); - } + mPlayingSoundID = mResult.mAmbientLoopSoundID; } - - stopSounds(false); + if (mAmbientSound.get()) + mAmbientSound->setVolume(mResult.mAmbientSoundVolume); } -void WeatherManager::stopSounds(bool stopAll) +void WeatherManager::stopSounds() { - std::vector::iterator it = mSoundsPlaying.begin(); - while (it!=mSoundsPlaying.end()) + if (mAmbientSound.get()) { - if (stopAll || - !((*it == mWeatherSettings[mCurrentWeather].mAmbientLoopSoundID) || - (*it == mWeatherSettings[mCurrentWeather].mRainLoopSoundID))) - { - MWBase::Environment::get().getSoundManager()->stopSound(*it); - it = mSoundsPlaying.erase(it); - } - else - ++it; + MWBase::Environment::get().getSoundManager()->stopSound(mAmbientSound); + mAmbientSound.reset(); + mPlayingSoundID.clear(); } } @@ -707,9 +724,13 @@ void WeatherManager::changeWeather(const std::string& region, const unsigned int mRegionOverrides[Misc::StringUtils::lowerCase(region)] = weather; - std::string playerRegion = MWBase::Environment::get().getWorld()->getPlayerPtr().getCell()->getCell()->mRegion; - if (Misc::StringUtils::ciEqual(region, playerRegion)) - setWeather(weather); + MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); + if (player.isInCell()) + { + std::string playerRegion = player.getCell()->getCell()->mRegion; + if (Misc::StringUtils::ciEqual(region, playerRegion)) + setWeather(weather); + } } void WeatherManager::modRegion(const std::string ®ionid, const std::vector &chances) @@ -748,10 +769,9 @@ void WeatherManager::write(ESM::ESMWriter& writer, Loading::Listener& progress) writer.startRecord(ESM::REC_WTHR); state.save(writer); writer.endRecord(ESM::REC_WTHR); - progress.increaseProgress(); } -bool WeatherManager::readRecord(ESM::ESMReader& reader, int32_t type) +bool WeatherManager::readRecord(ESM::ESMReader& reader, uint32_t type) { if(ESM::REC_WTHR == type) { @@ -759,14 +779,6 @@ bool WeatherManager::readRecord(ESM::ESMReader& reader, int32_t type) ESM::WeatherState state; state.load(reader); - // reset other temporary state, now that we loaded successfully - stopSounds(true); // let's hope this never throws - mRegionOverrides.clear(); - mRegionMods.clear(); - mThunderFlash = 0.0; - mThunderChance = 0.0; - mThunderChanceNeeded = 50.0; - // swap in the loaded values now that we can't fail mHour = state.mHour; mWindSpeed = state.mWindSpeed; @@ -783,6 +795,16 @@ bool WeatherManager::readRecord(ESM::ESMReader& reader, int32_t type) return false; } +void WeatherManager::clear() +{ + stopSounds(); + mRegionOverrides.clear(); + mRegionMods.clear(); + mThunderFlash = 0.0; + mThunderChance = 0.0; + mThunderChanceNeeded = 50.0; +} + void WeatherManager::switchToNextWeather(bool instantly) { MWBase::World* world = MWBase::Environment::get().getWorld(); diff --git a/apps/openmw/mwworld/weather.hpp b/apps/openmw/mwworld/weather.hpp index 292d06747..a2e668159 100644 --- a/apps/openmw/mwworld/weather.hpp +++ b/apps/openmw/mwworld/weather.hpp @@ -7,6 +7,8 @@ #include #include +#include "../mwbase/soundmanager.hpp" + namespace ESM { struct Region; @@ -61,10 +63,12 @@ namespace MWWorld bool mIsStorm; std::string mAmbientLoopSoundID; + float mAmbientSoundVolume; std::string mParticleEffect; - std::string mRainEffect; + float mEffectFade; + float mRainSpeed; float mRainFrequency; }; @@ -125,9 +129,6 @@ namespace MWWorld // This is used for Blight, Ashstorm and Blizzard (Bloodmoon) std::string mAmbientLoopSoundID; - // Rain sound effect - std::string mRainLoopSoundID; - // Is this an ash storm / blight storm? If so, the following will happen: // - The particles and clouds will be oriented so they appear to come from the Red Mountain. // - Characters will animate their hand to protect eyes from the storm when looking in its direction (idlestorm animation) @@ -169,10 +170,11 @@ namespace MWWorld /** * Per-frame update * @param duration + * @param paused */ - void update(float duration); + void update(float duration, bool paused = false); - void stopSounds(bool stopAll); + void stopSounds(); void setHour(const float hour); @@ -197,7 +199,9 @@ namespace MWWorld void write(ESM::ESMWriter& writer, Loading::Listener& progress); - bool readRecord(ESM::ESMReader& reader, int32_t type); + bool readRecord(ESM::ESMReader& reader, uint32_t type); + + void clear(); private: float mHour; @@ -205,6 +209,9 @@ namespace MWWorld bool mIsStorm; Ogre::Vector3 mStormDirection; + MWBase::SoundPtr mAmbientSound; + std::string mPlayingSoundID; + MWWorld::Fallback* mFallback; void setFallbackWeather(Weather& weather,const std::string& name); MWRender::RenderingManager* mRendering; @@ -213,8 +220,6 @@ namespace MWWorld std::map mRegionOverrides; - std::vector mSoundsPlaying; - std::string mCurrentWeather; std::string mNextWeather; diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 9b747582f..3ef4f8e81 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include @@ -43,6 +44,7 @@ #include "player.hpp" #include "manualref.hpp" +#include "cellstore.hpp" #include "cellfunctors.hpp" #include "containerstore.hpp" #include "inventorystore.hpp" @@ -51,7 +53,6 @@ #include "contentloader.hpp" #include "esmloader.hpp" -#include "omwloader.hpp" using namespace Ogre; @@ -148,9 +149,10 @@ namespace MWWorld mSky (true), mCells (mStore, mEsm), mActivationDistanceOverride (activationDistanceOverride), mFallback(fallbackMap), mTeleportEnabled(true), mLevitationEnabled(true), - mFacedDistance(FLT_MAX), mGodMode(false), mContentFiles (contentFiles), - mGoToJail(false), - mStartCell (startCell), mStartupScript(startupScript) + mGodMode(false), mContentFiles (contentFiles), + mGoToJail(false), mDaysInPrison(0), + mStartCell (startCell), mStartupScript(startupScript), + mScriptsEnabled(true) { mPhysics = new PhysicsSystem(renderer); mPhysEngine = mPhysics->getEngine(); @@ -163,19 +165,18 @@ namespace MWWorld mWeatherManager = new MWWorld::WeatherManager(mRendering,&mFallback); - // NOTE: We might need to reserve one more for the running game / save. mEsm.resize(contentFiles.size()); Loading::Listener* listener = MWBase::Environment::get().getWindowManager()->getLoadingScreen(); listener->loadingOn(); GameContentLoader gameContentLoader(*listener); EsmLoader esmLoader(mStore, mEsm, encoder, *listener); - OmwLoader omwLoader(*listener); gameContentLoader.addLoader(".esm", &esmLoader); gameContentLoader.addLoader(".esp", &esmLoader); - gameContentLoader.addLoader(".omwgame", &omwLoader); - gameContentLoader.addLoader(".omwaddon", &omwLoader); + gameContentLoader.addLoader(".omwgame", &esmLoader); + gameContentLoader.addLoader(".omwaddon", &esmLoader); + gameContentLoader.addLoader(".project", &esmLoader); loadContentFiles(fileCollections, contentFiles, gameContentLoader); @@ -188,6 +189,8 @@ namespace MWWorld mStore.setUp(); mStore.movePlayerRecord(); + mSwimHeightScale = mStore.get().find("fSwimHeightScale")->getFloat(); + mGlobalVariables.fill (mStore); mWorldScene = new Scene(*mRendering, mPhysics); @@ -203,6 +206,7 @@ namespace MWWorld setupPlayer(); renderPlayer(); + mRendering->resetCamera(); MWBase::Environment::get().getWindowManager()->updatePlayer(); @@ -211,9 +215,9 @@ namespace MWWorld // set new game mark mGlobalVariables["chargenstate"].setInteger (1); mGlobalVariables["pcrace"].setInteger (3); - - MWBase::Environment::get().getScriptManager()->getGlobalScripts().addStartup(); } + else + mGlobalVariables["chargenstate"].setInteger (-1); if (bypass && !mStartCell.empty()) { @@ -243,7 +247,7 @@ namespace MWWorld pos.rot[0] = 0; pos.rot[1] = 0; pos.rot[2] = 0; - mWorldScene->changeToExteriorCell(pos); + mWorldScene->changeToExteriorCell(pos, true); } } @@ -259,11 +263,13 @@ namespace MWWorld mWeatherManager = 0; mWeatherManager = new MWWorld::WeatherManager(mRendering,&mFallback); - MWBase::Environment::get().getWindowManager()->executeInConsole(mStartupScript); + if (!mStartupScript.empty()) + MWBase::Environment::get().getWindowManager()->executeInConsole(mStartupScript); } void World::clear() { + mWeatherManager->clear(); mRendering->clear(); mProjectileManager->clear(); @@ -288,9 +294,10 @@ namespace MWWorld mDoorStates.clear(); mGodMode = false; + mScriptsEnabled = true; mSky = true; mTeleportEnabled = true; - mFacedDistance = FLT_MAX; + mLevitationEnabled = true; mGlobalVariables.fill (mStore); } @@ -304,7 +311,14 @@ namespace MWWorld +mProjectileManager->countSavedGameRecords() +1 // player record +1 // weather record - +1; // actorId counter + +1 // actorId counter + +1 // levitation/teleport enabled state + +1; // camera + } + + int World::countSavedGameCells() const + { + return mCells.countSavedGameRecords(); } void World::write (ESM::ESMWriter& writer, Loading::Listener& progress) const @@ -318,68 +332,130 @@ namespace MWWorld } MWMechanics::CreatureStats::writeActorIdCounter(writer); - progress.increaseProgress(); + mStore.write (writer, progress); // dynamic Store must be written (and read) before Cells, so that + // references to custom made records will be recognized mCells.write (writer, progress); - mStore.write (writer, progress); mGlobalVariables.write (writer, progress); mPlayer->write (writer, progress); mWeatherManager->write (writer, progress); mProjectileManager->write (writer, progress); + + writer.startRecord(ESM::REC_ENAB); + writer.writeHNT("TELE", mTeleportEnabled); + writer.writeHNT("LEVT", mLevitationEnabled); + writer.endRecord(ESM::REC_ENAB); + + writer.startRecord(ESM::REC_CAM_); + writer.writeHNT("FIRS", isFirstPerson()); + writer.endRecord(ESM::REC_CAM_); } - void World::readRecord (ESM::ESMReader& reader, int32_t type, + void World::readRecord (ESM::ESMReader& reader, uint32_t type, const std::map& contentFileMap) { - if (type == ESM::REC_ACTC) - { - MWMechanics::CreatureStats::readActorIdCounter(reader); - return; - } - - if (!mStore.readRecord (reader, type) && - !mGlobalVariables.readRecord (reader, type) && - !mPlayer->readRecord (reader, type) && - !mWeatherManager->readRecord (reader, type) && - !mCells.readRecord (reader, type, contentFileMap) && - !mProjectileManager->readRecord (reader, type)) + switch (type) { - throw std::runtime_error ("unknown record in saved game"); + case ESM::REC_ACTC: + MWMechanics::CreatureStats::readActorIdCounter(reader); + return; + case ESM::REC_ENAB: + reader.getHNT(mTeleportEnabled, "TELE"); + reader.getHNT(mLevitationEnabled, "LEVT"); + return; + default: + if (!mStore.readRecord (reader, type) && + !mGlobalVariables.readRecord (reader, type) && + !mPlayer->readRecord (reader, type) && + !mWeatherManager->readRecord (reader, type) && + !mCells.readRecord (reader, type, contentFileMap) && + !mProjectileManager->readRecord (reader, type)) + { + throw std::runtime_error ("unknown record in saved game"); + } + break; } } void World::ensureNeededRecords() { - if (!mStore.get().search("sCompanionShare")) - { - ESM::GameSetting sCompanionShare; - sCompanionShare.mId = "sCompanionShare"; - ESM::Variant value; - value.setType(ESM::VT_String); - value.setString("Companion Share"); - sCompanionShare.mValue = value; - mStore.insertStatic(sCompanionShare); - } - if (!mStore.get().search("dayspassed")) - { - // vanilla Morrowind does not define dayspassed. - ESM::Global dayspassed; - dayspassed.mId = "dayspassed"; - ESM::Variant value; - value.setType(ESM::VT_Long); - value.setInteger(1); // but the addons start counting at 1 :( - dayspassed.mValue = value; - mStore.insertStatic(dayspassed); + std::map gmst; + // Companion (tribunal) + gmst["sCompanionShare"] = ESM::Variant("Companion Share"); + gmst["sCompanionWarningMessage"] = ESM::Variant("Warning message"); + gmst["sCompanionWarningButtonOne"] = ESM::Variant("Button 1"); + gmst["sCompanionWarningButtonTwo"] = ESM::Variant("Button 2"); + gmst["sCompanionShare"] = ESM::Variant("Companion Share"); + gmst["sProfitValue"] = ESM::Variant("Profit Value"); + gmst["sTeleportDisabled"] = ESM::Variant("Teleport disabled"); + gmst["sLevitateDisabled"] = ESM::Variant("Levitate disabled"); + + // Missing in unpatched MW 1.0 + gmst["sDifficulty"] = ESM::Variant("Difficulty"); + gmst["fDifficultyMult"] = ESM::Variant(5.f); + gmst["sAuto_Run"] = ESM::Variant("Auto Run"); + gmst["sServiceRefusal"] = ESM::Variant("Service Refusal"); + gmst["sNeedOneSkill"] = ESM::Variant("Need one skill"); + gmst["sNeedTwoSkills"] = ESM::Variant("Need two skills"); + gmst["sEasy"] = ESM::Variant("Easy"); + gmst["sHard"] = ESM::Variant("Hard"); + gmst["sDeleteNote"] = ESM::Variant("Delete Note"); + gmst["sEditNote"] = ESM::Variant("Edit Note"); + gmst["sAdmireSuccess"] = ESM::Variant("Admire Success"); + gmst["sAdmireFail"] = ESM::Variant("Admire Fail"); + gmst["sIntimidateSuccess"] = ESM::Variant("Intimidate Success"); + gmst["sIntimidateFail"] = ESM::Variant("Intimidate Fail"); + gmst["sTauntSuccess"] = ESM::Variant("Taunt Success"); + gmst["sTauntFail"] = ESM::Variant("Taunt Fail"); + gmst["sBribeSuccess"] = ESM::Variant("Bribe Success"); + gmst["sBribeFail"] = ESM::Variant("Bribe Fail"); + gmst["fNPCHealthBarTime"] = ESM::Variant(5.f); + gmst["fNPCHealthBarFade"] = ESM::Variant(1.f); + + // Werewolf (BM) + gmst["fWereWolfRunMult"] = ESM::Variant(1.f); + gmst["fWereWolfSilverWeaponDamageMult"] = ESM::Variant(1.f); + + + std::map globals; + // vanilla Morrowind does not define dayspassed. + globals["dayspassed"] = ESM::Variant(1); // but the addons start counting at 1 :( + globals["werewolfclawmult"] = ESM::Variant(25.f); + globals["pcknownwerewolf"] = ESM::Variant(0); + + // following should exist in all versions of MW, but not necessarily in TCs + globals["gamehour"] = ESM::Variant(0.f); + globals["timescale"] = ESM::Variant(30.f); + globals["day"] = ESM::Variant(1); + globals["month"] = ESM::Variant(1); + globals["year"] = ESM::Variant(1); + globals["pcrace"] = ESM::Variant(0); + globals["pchascrimegold"] = ESM::Variant(0); + globals["pchasgolddiscount"] = ESM::Variant(0); + globals["crimegolddiscount"] = ESM::Variant(0); + globals["crimegoldturnin"] = ESM::Variant(0); + globals["pchasturnin"] = ESM::Variant(0); + + for (std::map::iterator it = gmst.begin(); it != gmst.end(); ++it) + { + if (!mStore.get().search(it->first)) + { + ESM::GameSetting setting; + setting.mId = it->first; + setting.mValue = it->second; + mStore.insertStatic(setting); + } } - if (!mStore.get().search("fWereWolfRunMult")) + + for (std::map::iterator it = globals.begin(); it != globals.end(); ++it) { - ESM::GameSetting fWereWolfRunMult; - fWereWolfRunMult.mId = "fWereWolfRunMult"; - ESM::Variant value; - value.setType(ESM::VT_Float); - value.setFloat(1.f); - fWereWolfRunMult.mValue = value; - mStore.insertStatic(fWereWolfRunMult); + if (!mStore.get().search(it->first)) + { + ESM::Global setting; + setting.mId = it->first; + setting.mValue = it->second; + mStore.insertStatic(setting); + } } } @@ -546,28 +622,37 @@ namespace MWWorld std::string lowerCaseName = Misc::StringUtils::lowerCase(name); - // active cells for (Scene::CellStoreCollection::const_iterator iter (mWorldScene->getActiveCells().begin()); iter!=mWorldScene->getActiveCells().end(); ++iter) { + // TODO: caching still doesn't work efficiently here (only works for the one CellStore that the reference is in) CellStore* cellstore = *iter; - Ptr ptr = mCells.getPtr (lowerCaseName, *cellstore, true); + Ptr ptr = mCells.getPtr (lowerCaseName, *cellstore, false); if (!ptr.isEmpty()) return ptr; } - Ptr ptr = mPlayer->getPlayer().getClass() - .getContainerStore(mPlayer->getPlayer()).search(lowerCaseName); - - if (!ptr.isEmpty()) - return ptr; - if (!activeOnly) { ret = mCells.getPtr (lowerCaseName); + if (!ret.isEmpty()) + return ret; } - return ret; + + for (Scene::CellStoreCollection::const_iterator iter (mWorldScene->getActiveCells().begin()); + iter!=mWorldScene->getActiveCells().end(); ++iter) + { + CellStore* cellstore = *iter; + Ptr ptr = cellstore->searchInContainer(lowerCaseName); + if (!ptr.isEmpty()) + return ptr; + } + + Ptr ptr = mPlayer->getPlayer().getClass() + .getContainerStore(mPlayer->getPlayer()).search(lowerCaseName); + + return ptr; } Ptr World::getPtr (const std::string& name, bool activeOnly) @@ -603,6 +688,47 @@ namespace MWWorld return mWorldScene->searchPtrViaActorId (actorId); } + struct FindContainerFunctor + { + Ptr mContainedPtr; + Ptr mResult; + + FindContainerFunctor(const Ptr& containedPtr) : mContainedPtr(containedPtr) {} + + bool operator() (Ptr ptr) + { + if (mContainedPtr.getContainerStore() == &ptr.getClass().getContainerStore(ptr)) + { + mResult = ptr; + return false; + } + + return true; + } + }; + + Ptr World::findContainer(const Ptr& ptr) + { + if (ptr.isInCell()) + return Ptr(); + + Ptr player = getPlayerPtr(); + if (ptr.getContainerStore() == &player.getClass().getContainerStore(player)) + return player; + + const Scene::CellStoreCollection& collection = mWorldScene->getActiveCells(); + for (Scene::CellStoreCollection::const_iterator cellIt = collection.begin(); cellIt != collection.end(); ++cellIt) + { + FindContainerFunctor functor(ptr); + (*cellIt)->forEachContainer(functor); + + if (!functor.mResult.isEmpty()) + return functor.mResult; + } + + return Ptr(); + } + void World::addContainerScripts(const Ptr& reference, CellStore * cell) { if( reference.getTypeName()==typeid (ESM::Container).name() || @@ -840,6 +966,8 @@ namespace MWWorld void World::changeToInteriorCell (const std::string& cellName, const ESM::Position& position) { + mPhysics->clearQueuedMovement(); + if (mCurrentWorldSpace != cellName) { // changed worldspace @@ -855,6 +983,8 @@ namespace MWWorld void World::changeToExteriorCell (const ESM::Position& position) { + mPhysics->clearQueuedMovement(); + if (mCurrentWorldSpace != "sys::default") // FIXME { // changed worldspace @@ -862,7 +992,7 @@ namespace MWWorld mRendering->notifyWorldSpaceChanged(); } removeContainerScripts(getPlayerPtr()); - mWorldScene->changeToExteriorCell(position); + mWorldScene->changeToExteriorCell(position, true); addContainerScripts(getPlayerPtr(), getPlayerPtr().getCell()); } @@ -908,9 +1038,27 @@ namespace MWWorld MWWorld::Ptr World::getFacedObject() { - if (mFacedHandle.empty()) + std::string facedHandle; + + if (MWBase::Environment::get().getWindowManager()->isGuiMode() && + MWBase::Environment::get().getWindowManager()->isConsoleMode()) + getFacedHandle(facedHandle, getMaxActivationDistance() * 50, false); + else + { + float telekinesisRangeBonus = + mPlayer->getPlayer().getClass().getCreatureStats(mPlayer->getPlayer()).getMagicEffects() + .get(ESM::MagicEffect::Telekinesis).getMagnitude(); + telekinesisRangeBonus = feetToGameUnits(telekinesisRangeBonus); + + float activationDistance = getMaxActivationDistance() + telekinesisRangeBonus; + + getFacedHandle(facedHandle, activationDistance); + } + + if (facedHandle.empty()) return MWWorld::Ptr(); - return searchPtrViaHandle(mFacedHandle); + + return getPtrViaHandle(facedHandle); } std::pair World::getHitContact(const MWWorld::Ptr &ptr, float distance) @@ -940,7 +1088,7 @@ namespace MWWorld void World::deleteObject (const Ptr& ptr) { - if (ptr.getRefData().getCount() > 0) + if (!ptr.getRefData().isDeleted()) { ptr.getRefData().setCount(0); @@ -955,6 +1103,25 @@ namespace MWWorld } } + void World::undeleteObject(const Ptr& ptr) + { + if (!ptr.getCellRef().hasContentFile()) + return; + if (ptr.getRefData().isDeleted()) + { + ptr.getRefData().setCount(1); + if (mWorldScene->getActiveCells().find(ptr.getCell()) != mWorldScene->getActiveCells().end() + && ptr.getRefData().isEnabled()) + { + mWorldScene->addObjectToScene(ptr); + std::string script = ptr.getClass().getScript(ptr); + if (!script.empty()) + mLocalScripts.add(script, ptr); + addContainerScripts(ptr, ptr.getCell()); + } + } + } + void World::moveObject(const Ptr &ptr, CellStore* newCell, float x, float y, float z) { ESM::Position pos = ptr.getRefData().getPosition(); @@ -969,7 +1136,7 @@ namespace MWWorld CellStore *currCell = ptr.isInCell() ? ptr.getCell() : NULL; // currCell == NULL should only happen for player, during initial startup bool isPlayer = ptr == mPlayer->getPlayer(); - bool haveToMove = isPlayer || mWorldScene->isCellActive(*currCell); + bool haveToMove = isPlayer || (currCell && mWorldScene->isCellActive(*currCell)); if (currCell != newCell) { @@ -981,9 +1148,10 @@ namespace MWWorld changeToInteriorCell(Misc::StringUtils::lowerCase(newCell->getCell()->mName), pos); else { - int cellX = newCell->getCell()->getGridX(); - int cellY = newCell->getCell()->getGridY(); - mWorldScene->changeCell(cellX, cellY, pos, false); + if (mWorldScene->isCellActive(*newCell)) + mWorldScene->changePlayerCell(newCell, pos, false); + else + mWorldScene->changeToExteriorCell(pos, false); } addContainerScripts (getPlayerPtr(), newCell); } @@ -1021,6 +1189,7 @@ namespace MWWorld ptr.getClass().copyToCell(ptr, *newCell, pos); mRendering->updateObjectCell(ptr, copy); + ptr.getRefData().setBaseNode(NULL); MWBase::Environment::get().getSoundManager()->updatePtr (ptr, copy); MWBase::MechanicsManager *mechMgr = MWBase::Environment::get().getMechanicsManager(); @@ -1037,6 +1206,10 @@ namespace MWWorld } } ptr.getRefData().setCount(0); + // Deleted references can still be accessed by scripts, + // so we need this extra step to remove access to the old reference completely. + // This will no longer be necessary once we have a proper cell movement tracker. + ptr.getCellRef().unsetRefNum(); } } if (haveToMove && ptr.getRefData().getBaseNode()) @@ -1044,9 +1217,13 @@ namespace MWWorld mRendering->moveObject(ptr, vec); mPhysics->moveObject (ptr); } + if (isPlayer) + { + mWorldScene->playerMoved (vec); + } } - bool World::moveObjectImp(const Ptr& ptr, float x, float y, float z) + MWWorld::Ptr World::moveObjectImp(const Ptr& ptr, float x, float y, float z) { CellStore *cell = ptr.getCell(); @@ -1059,12 +1236,14 @@ namespace MWWorld moveObject(ptr, cell, x, y, z); - return cell != ptr.getCell(); + MWWorld::Ptr updated = ptr; + updated.mCell = cell; + return updated; } - void World::moveObject (const Ptr& ptr, float x, float y, float z) + MWWorld::Ptr World::moveObject (const Ptr& ptr, float x, float y, float z) { - moveObjectImp(ptr, x, y, z); + return moveObjectImp(ptr, x, y, z); } void World::scaleObject (const Ptr& ptr, float scale) @@ -1151,7 +1330,7 @@ namespace MWWorld } } - void World::adjustPosition(const Ptr &ptr) + void World::adjustPosition(const Ptr &ptr, bool force) { ESM::Position pos (ptr.getRefData().getPosition()); @@ -1170,9 +1349,9 @@ namespace MWWorld ptr.getRefData().setPosition(pos); - if (!isFlying(ptr)) + if (force || !isFlying(ptr)) { - Ogre::Vector3 traced = mPhysics->traceDown(ptr, 200); + Ogre::Vector3 traced = mPhysics->traceDown(ptr, 500); if (traced.z < pos.pos[2]) pos.pos[2] = traced.z; } @@ -1233,13 +1412,15 @@ namespace MWWorld void World::doPhysics(float duration) { + mPhysics->stepSimulation(duration); + processDoors(duration); mProjectileManager->update(duration); const PtrVelocityList &results = mPhysics->applyQueuedMovement(duration); PtrVelocityList::const_iterator player(results.end()); - for(PtrVelocityList::const_iterator iter(results.begin());iter != results.end();iter++) + for(PtrVelocityList::const_iterator iter(results.begin());iter != results.end();++iter) { if(iter->first.getRefData().getHandle() == "player") { @@ -1251,8 +1432,6 @@ namespace MWWorld } if(player != results.end()) moveObjectImp(player->first, player->second.x, player->second.y, player->second.z); - - mPhysics->stepSimulation(duration); } bool World::castRay (float x1, float y1, float z1, float x2, float y2, float z2) @@ -1284,7 +1463,8 @@ namespace MWWorld bool reached = (targetRot == 90.f && it->second) || targetRot == 0.f; /// \todo should use convexSweepTest here - std::vector collisions = mPhysics->getCollisions(it->first); + std::vector collisions = mPhysics->getCollisions(it->first, OEngine::Physic::CollisionType_Actor + , OEngine::Physic::CollisionType_Actor); for (std::vector::iterator cit = collisions.begin(); cit != collisions.end(); ++cit) { MWWorld::Ptr ptr = getPtrViaHandle(*cit); @@ -1300,7 +1480,6 @@ namespace MWWorld // we need to undo the rotation localRotateObject(it->first, 0, 0, oldRot); reached = false; - //break; //Removed in case multiple actors are touching } } @@ -1346,6 +1525,16 @@ namespace MWWorld return mStore.insert(record); } + const ESM::CreatureLevList *World::createOverrideRecord(const ESM::CreatureLevList &record) + { + return mStore.overrideRecord(record); + } + + const ESM::ItemLevList *World::createOverrideRecord(const ESM::ItemLevList &record) + { + return mStore.overrideRecord(record); + } + const ESM::NPC *World::createRecord(const ESM::NPC &record) { bool update = false; @@ -1408,7 +1597,7 @@ namespace MWWorld if (mGoToJail && !paused) goToJail(); - updateWeather(duration); + updateWeather(duration, paused); if (!paused) doPhysics (duration); @@ -1419,6 +1608,8 @@ namespace MWWorld updateWindowManager (); + updateSoundListener(); + if (!paused && mPlayer->getPlayer().getCell()->isExterior()) { ESM::Position pos = mPlayer->getPlayer().getRefData().getPosition(); @@ -1426,6 +1617,18 @@ namespace MWWorld } } + void World::updateSoundListener() + { + Ogre::Vector3 playerPos = mPlayer->getPlayer().getRefData().getBaseNode()->getPosition(); + const OEngine::Physic::PhysicActor *actor = mPhysEngine->getCharacter(getPlayerPtr().getRefData().getHandle()); + if(actor) playerPos.z += 1.85*actor->getHalfExtents().z; + Ogre::Quaternion playerOrient = Ogre::Quaternion(Ogre::Radian(getPlayerPtr().getRefData().getPosition().rot[2]), Ogre::Vector3::NEGATIVE_UNIT_Z) * + Ogre::Quaternion(Ogre::Radian(getPlayerPtr().getRefData().getPosition().rot[0]), Ogre::Vector3::NEGATIVE_UNIT_X) * + Ogre::Quaternion(Ogre::Radian(getPlayerPtr().getRefData().getPosition().rot[1]), Ogre::Vector3::NEGATIVE_UNIT_Y); + MWBase::Environment::get().getSoundManager()->setListenerPosDir(playerPos, playerOrient.yAxis(), + playerOrient.zAxis()); + } + void World::updateWindowManager () { // inform the GUI about focused object @@ -1458,70 +1661,38 @@ namespace MWWorld Vector3 sun = mRendering->getSkyManager()->getRealSunPos(); mRendering->getSkyManager()->setGlare(!mPhysics->castRay(Ogre::Vector3(p[0], p[1], p[2]), sun)); } - - updateFacedHandle (); } - void World::updateFacedHandle () + void World::getFacedHandle(std::string& facedHandle, float maxDistance, bool ignorePlayer) { - float telekinesisRangeBonus = - mPlayer->getPlayer().getClass().getCreatureStats(mPlayer->getPlayer()).getMagicEffects() - .get(ESM::MagicEffect::Telekinesis).mMagnitude; - telekinesisRangeBonus = feetToGameUnits(telekinesisRangeBonus); - - float activationDistance = getMaxActivationDistance() + telekinesisRangeBonus; - activationDistance += mRendering->getCameraDistance(); + maxDistance += mRendering->getCameraDistance(); - // send new query - // figure out which object we want to test against std::vector < std::pair < float, std::string > > results; if (MWBase::Environment::get().getWindowManager()->isGuiMode()) { float x, y; MWBase::Environment::get().getWindowManager()->getMousePosition(x, y); - results = mPhysics->getFacedHandles(x, y, activationDistance); - if (MWBase::Environment::get().getWindowManager()->isConsoleMode()) - results = mPhysics->getFacedHandles(x, y, getMaxActivationDistance ()*50); + results = mPhysics->getFacedHandles(x, y, maxDistance); } else { - results = mPhysics->getFacedHandles(activationDistance); + results = mPhysics->getFacedHandles(maxDistance); } - // ignore the player and other things we're not interested in - std::vector < std::pair < float, std::string > >::iterator it = results.begin(); - while (it != results.end()) - { - if ((*it).second.find("HeightField") != std::string::npos) // Don't attempt to getPtrViaHandle on terrain - { - ++it; - continue; - } - - if (getPtrViaHandle((*it).second) == mPlayer->getPlayer() ) // not interested in player (unless you want to talk to yourself) - { - it = results.erase(it); - } - else - ++it; - } + if (ignorePlayer && + !results.empty() && results.front().second == "player") + results.erase(results.begin()); if (results.empty() || results.front().second.find("HeightField") != std::string::npos) // Blocked by terrain - { - mFacedHandle = ""; - mFacedDistance = FLT_MAX; - } + facedHandle = ""; else - { - mFacedHandle = results.front().second; - mFacedDistance = results.front().first; - } + facedHandle = results.front().second; } bool World::isCellExterior() const { - CellStore *currentCell = mWorldScene->getCurrentCell(); + const CellStore *currentCell = mWorldScene->getCurrentCell(); if (currentCell) { return currentCell->getCell()->isExterior(); @@ -1531,7 +1702,7 @@ namespace MWWorld bool World::isCellQuasiExterior() const { - CellStore *currentCell = mWorldScene->getCurrentCell(); + const CellStore *currentCell = mWorldScene->getCurrentCell(); if (currentCell) { if (!(currentCell->getCell()->mData.mFlags & ESM::Cell::QuasiEx)) @@ -1557,19 +1728,15 @@ namespace MWWorld mWeatherManager->modRegion(regionid, chances); } - OEngine::Render::Fader* World::getFader() - { - return mRendering->getFader(); - } - Ogre::Vector2 World::getNorthVector (CellStore* cell) { MWWorld::CellRefList& statics = cell->get(); MWWorld::LiveCellRef* ref = statics.find("northmarker"); if (!ref) return Vector2(0, 1); - Ogre::SceneNode* node = ref->mData.getBaseNode(); - Vector3 dir = node->_getDerivedOrientation() * Ogre::Vector3(0,1,0); + + Ogre::Quaternion orient (Ogre::Radian(-ref->mData.getPosition().rot[2]), Ogre::Vector3::UNIT_Z); + Vector3 dir = orient * Ogre::Vector3(0,1,0); Vector2 d = Vector2(dir.x, dir.y); return d; } @@ -1590,6 +1757,23 @@ namespace MWWorld World::DoorMarker newMarker; newMarker.name = MWClass::Door::getDestination(ref); + ESM::CellId cellid; + if (!ref.mRef.getDestCell().empty()) + { + cellid.mWorldspace = ref.mRef.getDestCell(); + cellid.mPaged = false; + } + else + { + cellid.mPaged = true; + MWBase::Environment::get().getWorld()->positionToIndex( + ref.mRef.getDoorDest().pos[0], + ref.mRef.getDoorDest().pos[1], + cellid.mIndex.mX, + cellid.mIndex.mY); + } + newMarker.dest = cellid; + ESM::Position pos = ref.mData.getPosition (); newMarker.x = pos.pos[0]; @@ -1599,9 +1783,14 @@ namespace MWWorld } } - void World::getInteriorMapPosition (Ogre::Vector2 position, float& nX, float& nY, int &x, int& y) + void World::worldToInteriorMapPosition (Ogre::Vector2 position, float& nX, float& nY, int &x, int& y) { - mRendering->getInteriorMapPosition(position, nX, nY, x, y); + mRendering->worldToInteriorMapPosition(position, nX, nY, x, y); + } + + Ogre::Vector2 World::interiorMapToWorldPosition(float nX, float nY, int x, int y) + { + return mRendering->interiorMapToWorldPosition(nX, nY, x, y); } bool World::isPositionExplored (float nX, float nY, int x, int y, bool interior) @@ -1611,6 +1800,7 @@ namespace MWWorld void World::setWaterHeight(const float height) { + mPhysics->setWaterHeight(height); mRendering->setWaterHeight(height); } @@ -1619,6 +1809,11 @@ namespace MWWorld return mRendering->toggleWater(); } + bool World::toggleWorld() + { + return mRendering->toggleWorld(); + } + void World::PCDropped (const Ptr& item) { std::string script = item.getClass().getScript(item); @@ -1720,6 +1915,7 @@ namespace MWWorld localRotation.rot[2] = 0; dropped.getRefData().setLocalRotation(localRotation); dropped.getCellRef().setPosition(pos); + dropped.getCellRef().unsetRefNum(); if (mWorldScene->isCellActive(*cell)) { if (dropped.getRefData().isEnabled()) { @@ -1778,11 +1974,10 @@ namespace MWWorld mRendering->getTriangleBatchCount(triangles, batches); } - bool - World::isFlying(const MWWorld::Ptr &ptr) const + bool World::isFlying(const MWWorld::Ptr &ptr) const { const MWMechanics::CreatureStats &stats = ptr.getClass().getCreatureStats(ptr); - bool isParalyzed = (stats.getMagicEffects().get(ESM::MagicEffect::Paralyze).mMagnitude > 0); + bool isParalyzed = (stats.getMagicEffects().get(ESM::MagicEffect::Paralyze).getMagnitude() > 0); if(!ptr.getClass().isActor()) return false; @@ -1793,7 +1988,7 @@ namespace MWWorld if (ptr.getClass().canFly(ptr)) return !isParalyzed; - if(stats.getMagicEffects().get(ESM::MagicEffect::Levitate).mMagnitude > 0 + if(stats.getMagicEffects().get(ESM::MagicEffect::Levitate).getMagnitude() > 0 && isLevitationEnabled()) return true; @@ -1804,14 +1999,13 @@ namespace MWWorld return false; } - bool - World::isSlowFalling(const MWWorld::Ptr &ptr) const + bool World::isSlowFalling(const MWWorld::Ptr &ptr) const { if(!ptr.getClass().isActor()) return false; const MWMechanics::CreatureStats &stats = ptr.getClass().getCreatureStats(ptr); - if(stats.getMagicEffects().get(ESM::MagicEffect::SlowFall).mMagnitude > 0) + if(stats.getMagicEffects().get(ESM::MagicEffect::SlowFall).getMagnitude() > 0) return true; return false; @@ -1819,31 +2013,35 @@ namespace MWWorld bool World::isSubmerged(const MWWorld::Ptr &object) const { - const float *fpos = object.getRefData().getPosition().pos; - Ogre::Vector3 pos(fpos[0], fpos[1], fpos[2]); + return isUnderwater(object, 1.0/mSwimHeightScale); + } - const OEngine::Physic::PhysicActor *actor = mPhysEngine->getCharacter(object.getRefData().getHandle()); - if(actor) pos.z += 1.85*actor->getHalfExtents().z; + bool World::isSwimming(const MWWorld::Ptr &object) const + { + return isUnderwater(object, mSwimHeightScale); + } - return isUnderwater(object.getCell(), pos); + bool World::isWading(const MWWorld::Ptr &object) const + { + const float kneeDeep = 0.25f; + return isUnderwater(object, kneeDeep); } - bool - World::isSwimming(const MWWorld::Ptr &object) const + bool World::isUnderwater(const MWWorld::Ptr &object, const float heightRatio) const { - /// \todo add check ifActor() - only actors can swim const float *fpos = object.getRefData().getPosition().pos; Ogre::Vector3 pos(fpos[0], fpos[1], fpos[2]); - /// \fixme 3/4ths submerged? const OEngine::Physic::PhysicActor *actor = mPhysEngine->getCharacter(object.getRefData().getHandle()); - if(actor) pos.z += actor->getHalfExtents().z * 1.5; + if (actor) + { + pos.z += heightRatio*2*actor->getHalfExtents().z; + } return isUnderwater(object.getCell(), pos); } - bool - World::isUnderwater(const MWWorld::CellStore* cell, const Ogre::Vector3 &pos) const + bool World::isUnderwater(const MWWorld::CellStore* cell, const Ogre::Vector3 &pos) const { if (!(cell->getCell()->mData.mFlags & ESM::Cell::HasWater)) { return false; @@ -1864,7 +2062,7 @@ namespace MWWorld bool World::isOnGround(const MWWorld::Ptr &ptr) const { RefData &refdata = ptr.getRefData(); - const OEngine::Physic::PhysicActor *physactor = mPhysEngine->getCharacter(refdata.getHandle()); + OEngine::Physic::PhysicActor *physactor = mPhysEngine->getCharacter(refdata.getHandle()); if(!physactor) return false; @@ -1882,7 +2080,7 @@ namespace MWWorld mPhysEngine); if(tracer.mFraction < 1.0f) // collision, must be close to something below { - const_cast (physactor)->setOnGround(true); + physactor->setOnGround(true); return true; } else @@ -1926,8 +2124,9 @@ namespace MWWorld // so we should make sure not to use a "stale" controller for that. MWBase::Environment::get().getMechanicsManager()->add(mPlayer->getPlayer()); - mPhysics->addActor(mPlayer->getPlayer()); - mRendering->resetCamera(); + std::string model = getPlayerPtr().getClass().getModel(getPlayerPtr()); + model = Misc::ResourceHelpers::correctActorModelPath(model); + mPhysics->addActor(mPlayer->getPlayer(), model); } int World::canRest () @@ -1939,7 +2138,10 @@ namespace MWWorld Ogre::Vector3 playerPos(refdata.getPosition().pos); const OEngine::Physic::PhysicActor *physactor = mPhysEngine->getCharacter(refdata.getHandle()); - if((!physactor->getOnGround()&&physactor->getCollisionMode()) || isUnderwater(currentCell, playerPos)) + if (!physactor) + throw std::runtime_error("can't find player"); + + if((!physactor->getOnGround()&&physactor->getCollisionMode()) || isUnderwater(currentCell, playerPos) || isWalkingOnWater(player)) return 2; if((currentCell->getCell()->mData.mFlags&ESM::Cell::NoSleep) || player.getClass().getNpcStats(player).isWerewolf()) @@ -1996,18 +2198,90 @@ namespace MWWorld bool World::getPlayerStandingOn (const MWWorld::Ptr& object) { - MWWorld::Ptr player = mPlayer->getPlayer(); - if (!mPhysEngine->getCharacter("player")->getOnGround()) - return false; - btVector3 from (player.getRefData().getPosition().pos[0], player.getRefData().getPosition().pos[1], player.getRefData().getPosition().pos[2]); - btVector3 to = from - btVector3(0,0,5); - std::pair result = mPhysEngine->rayTest(from, to); - return result.first == object.getRefData().getBaseNode()->getName(); + MWWorld::Ptr player = getPlayerPtr(); + return mPhysics->isActorStandingOn(player, object); } bool World::getActorStandingOn (const MWWorld::Ptr& object) { - return mPhysEngine->isAnyActorStandingOn(object.getRefData().getBaseNode()->getName()); + std::vector actors; + mPhysics->getActorsStandingOn(object, actors); + return !actors.empty(); + } + + bool World::getPlayerCollidingWith (const MWWorld::Ptr& object) + { + MWWorld::Ptr player = getPlayerPtr(); + return mPhysics->isActorCollidingWith(player, object); + } + + bool World::getActorCollidingWith (const MWWorld::Ptr& object) + { + std::vector actors; + mPhysics->getActorsCollidingWith(object, actors); + return !actors.empty(); + } + + void World::hurtStandingActors(const Ptr &object, float healthPerSecond) + { + if (MWBase::Environment::get().getWindowManager()->isGuiMode()) + return; + + std::vector actors; + mPhysics->getActorsStandingOn(object, actors); + for (std::vector::iterator it = actors.begin(); it != actors.end(); ++it) + { + MWWorld::Ptr actor = searchPtrViaHandle(*it); // Collision events are from the last frame, actor might no longer exist + if (actor.isEmpty()) + continue; + + MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor); + if (stats.isDead()) + continue; + MWMechanics::DynamicStat health = stats.getHealth(); + health.setCurrent(health.getCurrent()-healthPerSecond*MWBase::Environment::get().getFrameDuration()); + stats.setHealth(health); + + if (healthPerSecond > 0.0f) + { + if (actor.getRefData().getHandle() == "player") + MWBase::Environment::get().getWindowManager()->activateHitOverlay(false); + + if (!MWBase::Environment::get().getSoundManager()->getSoundPlaying(actor, "Health Damage")) + MWBase::Environment::get().getSoundManager()->playSound3D(actor, "Health Damage", 1.0f, 1.0f); + } + } + } + + void World::hurtCollidingActors(const Ptr &object, float healthPerSecond) + { + if (MWBase::Environment::get().getWindowManager()->isGuiMode()) + return; + + std::vector actors; + mPhysics->getActorsCollidingWith(object, actors); + for (std::vector::iterator it = actors.begin(); it != actors.end(); ++it) + { + MWWorld::Ptr actor = searchPtrViaHandle(*it); // Collision events are from the last frame, actor might no longer exist + if (actor.isEmpty()) + continue; + + MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor); + if (stats.isDead()) + continue; + MWMechanics::DynamicStat health = stats.getHealth(); + health.setCurrent(health.getCurrent()-healthPerSecond*MWBase::Environment::get().getFrameDuration()); + stats.setHealth(health); + + if (healthPerSecond > 0.0f) + { + if (actor.getRefData().getHandle() == "player") + MWBase::Environment::get().getWindowManager()->activateHitOverlay(false); + + if (!MWBase::Environment::get().getSoundManager()->getSoundPlaying(actor, "Health Damage")) + MWBase::Environment::get().getSoundManager()->playSound3D(actor, "Health Damage", 1.0f, 1.0f); + } + } } float World::getWindSpeed() @@ -2044,6 +2318,8 @@ namespace MWWorld for (CellRefList::List::iterator container = refList.begin(); container != refList.end(); ++container) { MWWorld::Ptr ptr (&*container, *cellIt); + if (ptr.getRefData().isDeleted()) + continue; if (Misc::StringUtils::ciEqual(ptr.getCellRef().getOwner(), npc.getCellRef().getRefId())) out.push_back(ptr); } @@ -2084,9 +2360,15 @@ namespace MWWorld if (!targetActor.getRefData().getBaseNode() || !targetActor.getRefData().getBaseNode()) return false; // not in active cell - Ogre::Vector3 halfExt1 = mPhysEngine->getCharacter(actor.getRefData().getHandle())->getHalfExtents(); + OEngine::Physic::PhysicActor* actor1 = mPhysEngine->getCharacter(actor.getRefData().getHandle()); + OEngine::Physic::PhysicActor* actor2 = mPhysEngine->getCharacter(targetActor.getRefData().getHandle()); + + if (!actor1 || !actor2) + return false; + + Ogre::Vector3 halfExt1 = actor1->getHalfExtents(); const float* pos1 = actor.getRefData().getPosition().pos; - Ogre::Vector3 halfExt2 = mPhysEngine->getCharacter(targetActor.getRefData().getHandle())->getHalfExtents(); + Ogre::Vector3 halfExt2 = actor2->getHalfExtents(); const float* pos2 = targetActor.getRefData().getPosition().pos; btVector3 from(pos1[0],pos1[1],pos1[2]+halfExt1.z*2*0.9); // eye level @@ -2114,13 +2396,14 @@ namespace MWWorld void World::enableActorCollision(const MWWorld::Ptr& actor, bool enable) { OEngine::Physic::PhysicActor *physicActor = mPhysEngine->getCharacter(actor.getRefData().getHandle()); - - physicActor->enableCollisionBody(enable); + if (physicActor) + physicActor->enableCollisionBody(enable); } bool World::findInteriorPosition(const std::string &name, ESM::Position &pos) { typedef MWWorld::CellRefList::List DoorList; + typedef MWWorld::CellRefList::List StaticList; pos.rot[0] = pos.rot[1] = pos.rot[2] = 0; pos.pos[0] = pos.pos[1] = pos.pos[2] = 0; @@ -2165,6 +2448,13 @@ namespace MWWorld } } } + // Fall back to the first static location. + const StaticList &statics = cellStore->get().mList; + if ( statics.begin() != statics.end() ) { + pos = statics.begin()->mRef.getPosition(); + return true; + } + return false; } @@ -2205,6 +2495,11 @@ namespace MWWorld return mLevitationEnabled; } + void World::reattachPlayerCamera() + { + mRendering->rebuildPtr(getPlayerPtr()); + } + void World::setWerewolf(const MWWorld::Ptr& actor, bool werewolf) { MWMechanics::NpcStats& npcStats = actor.getClass().getNpcStats(actor); @@ -2252,6 +2547,42 @@ namespace MWWorld windowManager->unsetForceHide(MWGui::GW_Inventory); windowManager->unsetForceHide(MWGui::GW_Magic); } + + windowManager->setWerewolfOverlay(werewolf); + + // Witnesses of the player's transformation will make them a globally known werewolf + std::vector closeActors; + MWBase::Environment::get().getMechanicsManager()->getActorsInRange(Ogre::Vector3(actor.getRefData().getPosition().pos), + getStore().get().search("fAlarmRadius")->getFloat(), + closeActors); + + bool detected = false, reported = false; + for (std::vector::const_iterator it = closeActors.begin(); it != closeActors.end(); ++it) + { + if (*it == actor) + continue; + + if (!it->getClass().isNpc()) + continue; + + if (getLOS(*it, actor) && MWBase::Environment::get().getMechanicsManager()->awarenessCheck(actor, *it)) + detected = true; + if (it->getClass().getCreatureStats(*it).getAiSetting(MWMechanics::CreatureStats::AI_Alarm).getModified() > 0) + reported = true; + } + + if (detected) + { + windowManager->messageBox("#{sWerewolfAlarmMessage}"); + setGlobalInt("pcknownwerewolf", 1); + + if (reported) + { + npcStats.setBounty(npcStats.getBounty()+ + MWBase::Environment::get().getWorld()->getStore().get().find("iWereWolfBounty")->getInt()); + windowManager->messageBox("#{sCrimeMessage}"); + } + } } } @@ -2275,6 +2606,17 @@ namespace MWWorld return mGodMode; } + bool World::toggleScripts() + { + mScriptsEnabled = !mScriptsEnabled; + return mScriptsEnabled; + } + + bool World::getScriptsEnabled() const + { + return mScriptsEnabled; + } + void World::loadContentFiles(const Files::Collections& fileCollections, const std::vector& content, ContentLoader& contentLoader) { @@ -2309,11 +2651,11 @@ namespace MWWorld if (!selectedSpell.empty()) { - const ESM::Spell* spell = getStore().get().search(selectedSpell); + const ESM::Spell* spell = getStore().get().find(selectedSpell); // Check mana MWMechanics::DynamicStat magicka = stats.getMagicka(); - if (magicka.getCurrent() < spell->mData.mCost) + if (magicka.getCurrent() < spell->mData.mCost && !(isPlayer && getGodModeState())) { message = "#{sMagicInsufficientSP}"; fail = true; @@ -2344,8 +2686,49 @@ namespace MWWorld { MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor); - // TODO: this only works for the player - MWWorld::Ptr target = getFacedObject(); + // Get the target to use for "on touch" effects + MWWorld::Ptr target; + float distance = 192.f; // ?? + + if (actor == getPlayerPtr()) + { + // For the player, use camera to aim + std::string facedHandle; + getFacedHandle(facedHandle, distance); + if (!facedHandle.empty()) + target = getPtrViaHandle(facedHandle); + } + else + { + // For NPCs use facing direction from Head node + Ogre::Vector3 origin(actor.getRefData().getPosition().pos); + MWRender::Animation *anim = mRendering->getAnimation(actor); + if(anim != NULL) + { + Ogre::Node *node = anim->getNode("Head"); + if (node == NULL) + node = anim->getNode("Bip01 Head"); + if(node != NULL) + origin += node->_getDerivedPosition(); + } + Ogre::Quaternion orient; + orient = Ogre::Quaternion(Ogre::Radian(actor.getRefData().getPosition().rot[2]), Ogre::Vector3::NEGATIVE_UNIT_Z) * + Ogre::Quaternion(Ogre::Radian(actor.getRefData().getPosition().rot[0]), Ogre::Vector3::NEGATIVE_UNIT_X); + Ogre::Vector3 direction = orient.yAxis(); + Ogre::Vector3 dest = origin + direction * distance; + + + std::vector > collisions = mPhysEngine->rayTest2(btVector3(origin.x, origin.y, origin.z), btVector3(dest.x, dest.y, dest.z)); + for (std::vector >::iterator cIt = collisions.begin(); cIt != collisions.end(); ++cIt) + { + MWWorld::Ptr collided = getPtrViaHandle(cIt->second); + if (collided != actor) + { + target = collided; + break; + } + } + } std::string selectedSpell = stats.getSpells().getSelectedSpell(); @@ -2355,11 +2738,11 @@ namespace MWWorld if (!selectedSpell.empty()) { - const ESM::Spell* spell = getStore().get().search(selectedSpell); - - // A power can be used once per 24h - if (spell->mData.mType == ESM::Spell::ST_Power) - stats.getSpells().usePower(spell->mId); + const ESM::Spell* spell = getStore().get().find(selectedSpell); + + // A power can be used once per 24h + if (spell->mData.mType == ESM::Spell::ST_Power) + stats.getSpells().usePower(spell->mId); cast.cast(spell); } @@ -2418,18 +2801,45 @@ namespace MWWorld { if (cell->isExterior()) return false; - MWWorld::CellRefList& doors = cell->get(); - CellRefList::List& refList = doors.mList; - // Check if any door in the cell leads to an exterior directly - for (CellRefList::List::iterator it = refList.begin(); it != refList.end(); ++it) - { - MWWorld::LiveCellRef& ref = *it; - if (ref.mRef.getTeleport() && ref.mRef.getDestCell().empty()) - { - ESM::Position pos = ref.mRef.getDoorDest(); - result = Ogre::Vector3(pos.pos); - return true; + // Search for a 'nearest' exterior, counting each cell between the starting + // cell and the exterior as a distance of 1. Will fail for isolated interiors. + std::set< std::string >checkedCells; + std::set< std::string >currentCells; + std::set< std::string >nextCells; + nextCells.insert( cell->getCell()->mName ); + + while ( !nextCells.empty() ) { + currentCells = nextCells; + nextCells.clear(); + for( std::set< std::string >::const_iterator i = currentCells.begin(); i != currentCells.end(); ++i ) { + MWWorld::CellStore *next = getInterior( *i ); + if ( !next ) continue; + + const MWWorld::CellRefList& doors = next->getReadOnly(); + const CellRefList::List& refList = doors.mList; + + // Check if any door in the cell leads to an exterior directly + for (CellRefList::List::const_iterator it = refList.begin(); it != refList.end(); ++it) + { + const MWWorld::LiveCellRef& ref = *it; + if (!ref.mRef.getTeleport()) continue; + + if (ref.mRef.getDestCell().empty()) + { + ESM::Position pos = ref.mRef.getDoorDest(); + result = Ogre::Vector3(pos.pos); + return true; + } + else + { + std::string dest = ref.mRef.getDestCell(); + if ( !checkedCells.count(dest) && !currentCells.count(dest) ) + nextCells.insert(dest); + } + } + + checkedCells.insert( *i ); } } @@ -2437,45 +2847,114 @@ namespace MWWorld return false; } - void World::teleportToClosestMarker (const MWWorld::Ptr& ptr, - const std::string& id) + MWWorld::Ptr World::getClosestMarker( const MWWorld::Ptr &ptr, const std::string &id ) { - Ogre::Vector3 worldPos; - if (!findInteriorPositionInWorldSpace(ptr.getCell(), worldPos)) - worldPos = mPlayer->getLastKnownExteriorPosition(); + if ( ptr.getCell()->isExterior() ) { + return getClosestMarkerFromExteriorPosition(mPlayer->getLastKnownExteriorPosition(), id); + } + + // Search for a 'nearest' marker, counting each cell between the starting + // cell and the exterior as a distance of 1. If an exterior is found, jump + // to the nearest exterior marker, without further interior searching. + std::set< std::string >checkedCells; + std::set< std::string >currentCells; + std::set< std::string >nextCells; + MWWorld::Ptr closestMarker; + + nextCells.insert( ptr.getCell()->getCell()->mName ); + while ( !nextCells.empty() ) { + currentCells = nextCells; + nextCells.clear(); + for( std::set< std::string >::const_iterator i = currentCells.begin(); i != currentCells.end(); ++i ) { + MWWorld::CellStore *next = getInterior( *i ); + checkedCells.insert( *i ); + if ( !next ) continue; + + closestMarker = next->search( id ); + if ( !closestMarker.isEmpty() ) + { + return closestMarker; + } + + const MWWorld::CellRefList& doors = next->getReadOnly(); + const CellRefList::List& doorList = doors.mList; + + // Check if any door in the cell leads to an exterior directly + for (CellRefList::List::const_iterator it = doorList.begin(); it != doorList.end(); ++it) + { + const MWWorld::LiveCellRef& ref = *it; + + if (!ref.mRef.getTeleport()) continue; + if (ref.mRef.getDestCell().empty()) + { + Ogre::Vector3 worldPos = Ogre::Vector3(ref.mRef.getDoorDest().pos); + return getClosestMarkerFromExteriorPosition(worldPos, id); + } + else + { + std::string dest = ref.mRef.getDestCell(); + if ( !checkedCells.count(dest) && !currentCells.count(dest) ) + nextCells.insert(dest); + } + } + } + } + + return MWWorld::Ptr(); + } + + MWWorld::Ptr World::getClosestMarkerFromExteriorPosition( const Ogre::Vector3 worldPos, const std::string &id ) { MWWorld::Ptr closestMarker; float closestDistance = FLT_MAX; std::vector markers; mCells.getExteriorPtrs(id, markers); - - for (std::vector::iterator it = markers.begin(); it != markers.end(); ++it) + for (std::vector::iterator it2 = markers.begin(); it2 != markers.end(); ++it2) { - ESM::Position pos = it->getRefData().getPosition(); + ESM::Position pos = it2->getRefData().getPosition(); Ogre::Vector3 markerPos = Ogre::Vector3(pos.pos); float distance = worldPos.squaredDistance(markerPos); if (distance < closestDistance) { closestDistance = distance; - closestMarker = *it; + closestMarker = *it2; } } - MWWorld::ActionTeleport action("", closestMarker.getRefData().getPosition()); - action.execute(ptr); + return closestMarker; } - void World::updateWeather(float duration) + + void World::teleportToClosestMarker (const MWWorld::Ptr& ptr, + const std::string& id) + { + MWWorld::Ptr closestMarker = getClosestMarker( ptr, id ); + + if ( closestMarker.isEmpty() ) + { + std::cerr << "Failed to teleport: no closest marker found" << std::endl; + return; + } + + std::string cellName; + if ( !closestMarker.mCell->isExterior() ) + cellName = closestMarker.mCell->getCell()->mName; + + MWWorld::ActionTeleport action(cellName, closestMarker.getRefData().getPosition()); + action.execute(ptr); + } + + void World::updateWeather(float duration, bool paused) { if (mPlayer->wasTeleported()) { mPlayer->setTeleported(false); mWeatherManager->switchToNextWeather(true); } - - mWeatherManager->update(duration); + + mWeatherManager->update(duration, paused); } struct AddDetectedReference @@ -2533,6 +3012,9 @@ namespace MWWorld } else if (ptr.getClass().getTypeName() != typeid(ESM::Creature).name()) return false; + + if (ptr.getClass().getCreatureStats(ptr).isDead()) + return false; } if (mType == World::Detect_Key && !ptr.getClass().isKey(ptr)) return false; @@ -2547,11 +3029,11 @@ namespace MWWorld const MWMechanics::MagicEffects& effects = ptr.getClass().getCreatureStats(ptr).getMagicEffects(); float dist=0; if (type == World::Detect_Creature) - dist = effects.get(ESM::MagicEffect::DetectAnimal).mMagnitude; + dist = effects.get(ESM::MagicEffect::DetectAnimal).getMagnitude(); else if (type == World::Detect_Key) - dist = effects.get(ESM::MagicEffect::DetectKey).mMagnitude; + dist = effects.get(ESM::MagicEffect::DetectKey).getMagnitude(); else if (type == World::Detect_Enchantment) - dist = effects.get(ESM::MagicEffect::DetectEnchantment).mMagnitude; + dist = effects.get(ESM::MagicEffect::DetectEnchantment).getMagnitude(); if (!dist) return; @@ -2609,118 +3091,60 @@ namespace MWWorld void World::confiscateStolenItems(const Ptr &ptr) { - Ogre::Vector3 playerPos; - if (!findInteriorPositionInWorldSpace(ptr.getCell(), playerPos)) - playerPos = mPlayer->getLastKnownExteriorPosition(); - - MWWorld::Ptr closestChest; - float closestDistance = FLT_MAX; - - //Find closest stolen_goods chest - std::vector chests; - mCells.getInteriorPtrs("stolen_goods", chests); - - Ogre::Vector3 chestPos; - for (std::vector::iterator it = chests.begin(); it != chests.end(); ++it) + MWWorld::Ptr prisonMarker = getClosestMarker( ptr, "prisonmarker" ); + if ( prisonMarker.isEmpty() ) { - if (!findInteriorPositionInWorldSpace(it->getCell(), chestPos)) - continue; - - float distance = playerPos.squaredDistance(chestPos); - if (distance < closestDistance) - { - closestDistance = distance; - closestChest = *it; - } + std::cerr << "Failed to confiscate items: no closest prison marker found." << std::endl; + return; + } + std::string prisonName = prisonMarker.mRef->mRef.getDestCell(); + if ( prisonName.empty() ) + { + std::cerr << "Failed to confiscate items: prison marker not linked to prison interior" << std::endl; + return; + } + MWWorld::CellStore *prison = getInterior( prisonName ); + if ( !prison ) + { + std::cerr << "Failed to confiscate items: failed to load cell " << prisonName << std::endl; + return; } + MWWorld::Ptr closestChest = prison->search( "stolen_goods" ); if (!closestChest.isEmpty()) //Found a close chest { - ContainerStore& store = ptr.getClass().getContainerStore(ptr); - for (ContainerStoreIterator it = store.begin(); it != store.end(); ++it) //Move all stolen stuff into chest - { - if (!it->getCellRef().getOwner().empty() && it->getCellRef().getOwner() != "player") //Not owned by no one/player? - { - closestChest.getClass().getContainerStore(closestChest).add(*it, it->getRefData().getCount(), closestChest); - store.remove(*it, it->getRefData().getCount(), ptr); - } - } - closestChest.getClass().lock(closestChest,50); + MWBase::Environment::get().getMechanicsManager()->confiscateStolenItems(ptr, closestChest); } + else + std::cerr << "Failed to confiscate items: no stolen_goods container found" << std::endl; } void World::goToJail() { if (!mGoToJail) { - // Save for next update, since the player should be able to read the dialog text first + // Reset bounty and forget the crime now, but don't change cell yet (the player should be able to read the dialog text first) mGoToJail = true; - return; - } - else - { - mGoToJail = false; - - MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_Dialogue); MWWorld::Ptr player = getPlayerPtr(); - teleportToClosestMarker(player, "prisonmarker"); + int bounty = player.getClass().getNpcStats(player).getBounty(); player.getClass().getNpcStats(player).setBounty(0); + mPlayer->recordCrimeId(); confiscateStolenItems(player); int iDaysinPrisonMod = getStore().get().find("iDaysinPrisonMod")->getInt(); - int days = std::max(1, bounty / iDaysinPrisonMod); - - advanceTime(days * 24); - for (int i=0; irest (true); - - std::set skills; - for (int day=0; day (RAND_MAX) + 1) * ESM::Skill::Length; - skills.insert(skill); - - MWMechanics::SkillValue& value = player.getClass().getNpcStats(player).getSkill(skill); - if (skill == ESM::Skill::Security || skill == ESM::Skill::Sneak) - value.setBase(std::min(100, value.getBase()+1)); - else - value.setBase(value.getBase()-1); - } - - const Store& gmst = getStore().get(); - - std::string message; - if (days == 1) - message = gmst.find("sNotifyMessage42")->getString(); - else - message = gmst.find("sNotifyMessage43")->getString(); + mDaysInPrison = std::max(1, bounty / iDaysinPrisonMod); - std::stringstream dayStr; - dayStr << days; - if (message.find("%d") != std::string::npos) - message.replace(message.find("%d"), 2, dayStr.str()); + return; + } + else + { + mGoToJail = false; - for (std::set::iterator it = skills.begin(); it != skills.end(); ++it) - { - std::string skillName = gmst.find(ESM::Skill::sSkillNameIds[*it])->getString(); - std::stringstream skillValue; - skillValue << player.getClass().getNpcStats(player).getSkill(*it).getBase(); - std::string skillMsg = gmst.find("sNotifyMessage44")->getString(); - if (*it == ESM::Skill::Sneak || *it == ESM::Skill::Security) - skillMsg = gmst.find("sNotifyMessage39")->getString(); - - if (skillMsg.find("%s") != std::string::npos) - skillMsg.replace(skillMsg.find("%s"), 2, skillName); - if (skillMsg.find("%d") != std::string::npos) - skillMsg.replace(skillMsg.find("%d"), 2, skillValue.str()); - message += "\n" + skillMsg; - } + MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_Dialogue); - std::vector buttons; - buttons.push_back("#{sOk}"); - MWBase::Environment::get().getWindowManager()->messageBox(message, buttons); + MWBase::Environment::get().getWindowManager()->goToJail(mDaysInPrison); } } @@ -2759,6 +3183,9 @@ namespace MWWorld void World::spawnBloodEffect(const Ptr &ptr, const Vector3 &worldPosition) { + if (ptr.getRefData().getHandle() == "player" && Settings::Manager::getBool("hit fader", "GUI")) + return; + int type = ptr.getClass().getBloodTexture(ptr); std::string texture; switch (type) @@ -2789,7 +3216,7 @@ namespace MWWorld mRendering->spawnEffect(model, textureOverride, worldPos); } - void World::explodeSpell(const Vector3 &origin, const ESM::EffectList &effects, const Ptr &caster, + void World::explodeSpell(const Vector3 &origin, const ESM::EffectList &effects, const Ptr &caster, ESM::RangeType rangeType, const std::string& id, const std::string& sourceName) { std::map > toApply; @@ -2825,7 +3252,6 @@ namespace MWWorld std::vector objects; MWBase::Environment::get().getMechanicsManager()->getObjectsInRange( origin, feetToGameUnits(effectIt->mArea), objects); - for (std::vector::iterator affected = objects.begin(); affected != objects.end(); ++affected) toApply[*affected].push_back(*effectIt); } @@ -2849,7 +3275,7 @@ namespace MWWorld cast.mStack = false; ESM::EffectList effects; effects.mList = apply->second; - cast.inflict(apply->first, caster, effects, ESM::RT_Target, false, true); + cast.inflict(apply->first, caster, effects, rangeType, false, true); } } @@ -2862,12 +3288,52 @@ namespace MWWorld breakInvisibility(actor); - if (!script.empty()) + if (mScriptsEnabled) { - getLocalScripts().setIgnore (object); - MWBase::Environment::get().getScriptManager()->run (script, interpreterContext); + if (!script.empty()) + { + getLocalScripts().setIgnore (object); + MWBase::Environment::get().getScriptManager()->run (script, interpreterContext); + } + if (!interpreterContext.hasActivationBeenHandled()) + interpreterContext.executeActivation(object, actor); } - if (!interpreterContext.hasActivationBeenHandled()) + else interpreterContext.executeActivation(object, actor); } + + struct ResetActorsFunctor + { + bool operator() (Ptr ptr) + { + // Can't reset actors that were moved to a different cell, because we don't know what cell they came from. + // This could be fixed once we properly track actor cell changes, but may not be desirable behaviour anyhow. + if (ptr.getClass().isActor() && ptr.getCellRef().hasContentFile()) + { + const ESM::Position& origPos = ptr.getCellRef().getPosition(); + MWBase::Environment::get().getWorld()->moveObject(ptr, origPos.pos[0], origPos.pos[1], origPos.pos[2]); + MWBase::Environment::get().getWorld()->rotateObject(ptr, origPos.rot[0], origPos.rot[1], origPos.rot[2]); + ptr.getClass().adjustPosition(ptr, false); + } + return true; + } + }; + void World::resetActors() + { + for (Scene::CellStoreCollection::const_iterator iter (mWorldScene->getActiveCells().begin()); + iter!=mWorldScene->getActiveCells().end(); ++iter) + { + CellStore* cellstore = *iter; + ResetActorsFunctor functor; + cellstore->forEach(functor); + } + } + + bool World::isWalkingOnWater(const Ptr &actor) + { + OEngine::Physic::PhysicActor* physicActor = mPhysEngine->getCharacter(actor.getRefData().getHandle()); + if (physicActor && physicActor->isWalkingOnWater()) + return true; + return false; + } } diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index dda442963..1db86f738 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -82,6 +82,7 @@ namespace MWWorld boost::shared_ptr mProjectileManager; bool mGodMode; + bool mScriptsEnabled; std::vector mContentFiles; // not implemented @@ -91,8 +92,6 @@ namespace MWWorld Ptr getPtrViaHandle (const std::string& handle, CellStore& cellStore); int mActivationDistanceOverride; - std::string mFacedHandle; - float mFacedDistance; std::string mStartupScript; @@ -101,21 +100,21 @@ namespace MWWorld std::string mStartCell; - void updateWeather(float duration); + void updateWeather(float duration, bool paused = false); int getDaysPerMonth (int month) const; void rotateObjectImp (const Ptr& ptr, Ogre::Vector3 rot, bool adjust); - bool moveObjectImp (const Ptr& ptr, float x, float y, float z); - ///< @return true if the active cell (cell player is in) changed + Ptr moveObjectImp (const Ptr& ptr, float x, float y, float z); + ///< @return an updated Ptr in case the Ptr's cell changes Ptr copyObjectToCell(const Ptr &ptr, CellStore* cell, ESM::Position pos, bool adjustPos=true); + void updateSoundListener(); void updateWindowManager (); void performUpdateSceneQueries (); - void updateFacedHandle (); + void getFacedHandle(std::string& facedHandle, float maxDistance, bool ignorePlayer=true); - float getMaxActivationDistance (); float getNpcActivationDistance (); float getObjectActivationDistance (); @@ -140,12 +139,20 @@ namespace MWWorld void loadContentFiles(const Files::Collections& fileCollections, const std::vector& content, ContentLoader& contentLoader); + float mSwimHeightScale; + bool isUnderwater(const MWWorld::Ptr &object, const float heightRatio) const; + ///< helper function for implementing isSwimming(), isSubmerged(), isWading() + bool mTeleportEnabled; bool mLevitationEnabled; bool mGoToJail; + int mDaysInPrison; float feetToGameUnits(float feet); + MWWorld::Ptr getClosestMarker( const MWWorld::Ptr &ptr, const std::string &id ); + MWWorld::Ptr getClosestMarkerFromExteriorPosition( const Ogre::Vector3 worldPos, const std::string &id ); + public: World (OEngine::Render::OgreRenderer& renderer, @@ -163,15 +170,13 @@ namespace MWWorld virtual void clear(); virtual int countSavedGameRecords() const; + virtual int countSavedGameCells() const; virtual void write (ESM::ESMWriter& writer, Loading::Listener& progress) const; - virtual void readRecord (ESM::ESMReader& reader, int32_t type, + virtual void readRecord (ESM::ESMReader& reader, uint32_t type, const std::map& contentFileMap); - virtual OEngine::Render::Fader* getFader(); - ///< \todo remove this function. Rendering details should not be exposed. - virtual CellStore *getExterior (int x, int y); virtual CellStore *getInterior (const std::string& name); @@ -184,6 +189,7 @@ namespace MWWorld virtual void setWaterHeight(const float height); virtual bool toggleWater(); + virtual bool toggleWorld(); virtual void adjustSky(); @@ -201,20 +207,23 @@ namespace MWWorld virtual LocalScripts& getLocalScripts(); virtual bool hasCellChanged() const; - ///< Has the player moved to a different cell, since the last frame? + ///< Has the set of active cells changed, since the last frame? virtual bool isCellExterior() const; virtual bool isCellQuasiExterior() const; virtual Ogre::Vector2 getNorthVector (CellStore* cell); - ///< get north vector (OGRE coordinates) for given interior cell + ///< get north vector for given interior cell virtual void getDoorMarkers (MWWorld::CellStore* cell, std::vector& out); ///< get a list of teleport door markers for a given cell, to be displayed on the local map - virtual void getInteriorMapPosition (Ogre::Vector2 position, float& nX, float& nY, int &x, int& y); - ///< see MWRender::LocalMap::getInteriorMapPosition + virtual void worldToInteriorMapPosition (Ogre::Vector2 position, float& nX, float& nY, int &x, int& y); + ///< see MWRender::LocalMap::worldToInteriorMapPosition + + virtual Ogre::Vector2 interiorMapToWorldPosition (float nX, float nY, int x, int y); + ///< see MWRender::LocalMap::interiorMapToWorldPosition virtual bool isPositionExplored (float nX, float nY, int x, int y, bool interior); ///< see MWRender::LocalMap::isPositionExplored @@ -260,8 +269,13 @@ namespace MWWorld virtual Ptr searchPtrViaActorId (int actorId); ///< Search is limited to the active cells. - virtual void adjustPosition (const Ptr& ptr); + virtual MWWorld::Ptr findContainer (const MWWorld::Ptr& ptr); + ///< Return a pointer to a liveCellRef which contains \a ptr. + /// \note Search is limited to the active cells. + + virtual void adjustPosition (const Ptr& ptr, bool force); ///< Adjust position after load to be on ground. Must be called after model load. + /// @param force do this even if the ptr is flying virtual void fixPosition (const Ptr& actor); ///< Attempt to fix position so that the Ptr is no longer inside collision geometry. @@ -333,8 +347,10 @@ namespace MWWorld virtual std::pair getHitContact(const MWWorld::Ptr &ptr, float distance); virtual void deleteObject (const Ptr& ptr); + virtual void undeleteObject (const Ptr& ptr); - virtual void moveObject (const Ptr& ptr, float x, float y, float z); + virtual MWWorld::Ptr moveObject (const Ptr& ptr, float x, float y, float z); + ///< @return an updated Ptr in case the Ptr's cell changes virtual void moveObject (const Ptr& ptr, CellStore* newCell, float x, float y, float z); virtual void scaleObject (const Ptr& ptr, float scale); @@ -349,6 +365,8 @@ namespace MWWorld virtual MWWorld::Ptr safePlaceObject(const MWWorld::Ptr& ptr, MWWorld::CellStore* cell, ESM::Position pos); ///< place an object in a "safe" location (ie not in the void, etc). Makes a copy of the Ptr. + virtual float getMaxActivationDistance(); + virtual void indexToPosition (int cellX, int cellY, float &x, float &y, bool centre = false) const; ///< Convert cell numbers to position. @@ -412,6 +430,14 @@ namespace MWWorld ///< Create a new record (of type book) in the ESM store. /// \return pointer to created record + virtual const ESM::CreatureLevList *createOverrideRecord (const ESM::CreatureLevList& record); + ///< Write this record to the ESM store, allowing it to override a pre-existing record with the same ID. + /// \return pointer to created record + + virtual const ESM::ItemLevList *createOverrideRecord (const ESM::ItemLevList& record); + ///< Write this record to the ESM store, allowing it to override a pre-existing record with the same ID. + /// \return pointer to created record + virtual void update (float duration, bool paused); virtual MWWorld::Ptr placeObject (const MWWorld::Ptr& object, float cursorX, float cursorY, int amount); @@ -438,12 +464,17 @@ namespace MWWorld virtual bool isSubmerged(const MWWorld::Ptr &object) const; virtual bool isSwimming(const MWWorld::Ptr &object) const; virtual bool isUnderwater(const MWWorld::CellStore* cell, const Ogre::Vector3 &pos) const; + virtual bool isWading(const MWWorld::Ptr &object) const; virtual bool isOnGround(const MWWorld::Ptr &ptr) const; virtual void togglePOV() { mRendering->togglePOV(); } + virtual bool isFirstPerson() const { + return mRendering->getCamera()->isFirstPerson(); + } + virtual void togglePreviewMode(bool enable) { mRendering->togglePreviewMode(enable); } @@ -475,10 +506,20 @@ namespace MWWorld /// update movement state of a non-teleport door as specified /// @param state see MWClass::setDoorState + /// @note throws an exception when invoked on a teleport door virtual void activateDoor(const MWWorld::Ptr& door, int state); virtual bool getPlayerStandingOn (const MWWorld::Ptr& object); ///< @return true if the player is standing on \a object virtual bool getActorStandingOn (const MWWorld::Ptr& object); ///< @return true if any actor is standing on \a object + virtual bool getPlayerCollidingWith(const MWWorld::Ptr& object); ///< @return true if the player is colliding with \a object + virtual bool getActorCollidingWith (const MWWorld::Ptr& object); ///< @return true if any actor is colliding with \a object + virtual void hurtStandingActors (const MWWorld::Ptr& object, float dmgPerSecond); + ///< Apply a health difference to any actors standing on \a object. + /// To hurt actors, healthPerSecond should be a positive value. For a negative value, actors will be healed. + virtual void hurtCollidingActors (const MWWorld::Ptr& object, float dmgPerSecond); + ///< Apply a health difference to any actors colliding with \a object. + /// To hurt actors, healthPerSecond should be a positive value. For a negative value, actors will be healed. + virtual float getWindSpeed(); virtual void getContainersOwnedBy (const MWWorld::Ptr& npc, std::vector& out); @@ -502,6 +543,7 @@ namespace MWWorld /// \todo Probably shouldn't be here virtual MWRender::Animation* getAnimation(const MWWorld::Ptr &ptr); + virtual void reattachPlayerCamera(); /// \todo this does not belong here virtual void frameStarted (float dt, bool paused); @@ -535,6 +577,9 @@ namespace MWWorld virtual bool toggleGodMode(); + virtual bool toggleScripts(); + virtual bool getScriptsEnabled() const; + /** * @brief startSpellCast attempt to start casting a spell. Might fail immediately if conditions are not met. * @param actor @@ -592,7 +637,7 @@ namespace MWWorld virtual void spawnEffect (const std::string& model, const std::string& textureOverride, const Ogre::Vector3& worldPos); virtual void explodeSpell (const Ogre::Vector3& origin, const ESM::EffectList& effects, - const MWWorld::Ptr& caster, const std::string& id, const std::string& sourceName); + const MWWorld::Ptr& caster, ESM::RangeType rangeType, const std::string& id, const std::string& sourceName); virtual void activate (const MWWorld::Ptr& object, const MWWorld::Ptr& actor); @@ -601,6 +646,11 @@ namespace MWWorld /// @see MWWorld::WeatherManager::getStormDirection virtual Ogre::Vector3 getStormDirection() const; + + /// Resets all actors in the current active cells to their original location within that cell. + virtual void resetActors(); + + virtual bool isWalkingOnWater (const MWWorld::Ptr& actor); }; } diff --git a/apps/openmw_test_suite/CMakeLists.txt b/apps/openmw_test_suite/CMakeLists.txt index 9fe7890ac..2ffb7ffa0 100644 --- a/apps/openmw_test_suite/CMakeLists.txt +++ b/apps/openmw_test_suite/CMakeLists.txt @@ -1,24 +1,18 @@ -# TODO: This should not be needed, check how it was done in FindGTEST -set(GMOCK_ROOT "/usr/include") -set(GMOCK_BUILD "/usr/lib") - find_package(GTest REQUIRED) -find_package(GMock REQUIRED) - -if (GTEST_FOUND AND GMOCK_FOUND) +if (GTEST_FOUND) include_directories(${GTEST_INCLUDE_DIRS}) - include_directories(${GMOCK_INCLUDE_DIRS}) file(GLOB UNITTEST_SRC_FILES components/misc/test_*.cpp + mwdialogue/test_*.cpp ) source_group(apps\\openmw_test_suite FILES openmw_test_suite.cpp ${UNITTEST_SRC_FILES}) add_executable(openmw_test_suite openmw_test_suite.cpp ${UNITTEST_SRC_FILES}) - target_link_libraries(openmw_test_suite ${GMOCK_BOTH_LIBRARIES} ${GTEST_BOTH_LIBRARIES} components) + target_link_libraries(openmw_test_suite ${GTEST_BOTH_LIBRARIES} components) # Fix for not visible pthreads functions for linker with glibc 2.15 if (UNIX AND NOT APPLE) target_link_libraries(openmw_test_suite ${CMAKE_THREAD_LIBS_INIT}) diff --git a/apps/openmw_test_suite/mwdialogue/test_keywordsearch.cpp b/apps/openmw_test_suite/mwdialogue/test_keywordsearch.cpp new file mode 100644 index 000000000..e0e1871d2 --- /dev/null +++ b/apps/openmw_test_suite/mwdialogue/test_keywordsearch.cpp @@ -0,0 +1,67 @@ +#include +#include "apps/openmw/mwdialogue/keywordsearch.hpp" + +struct KeywordSearchTest : public ::testing::Test +{ + protected: + virtual void SetUp() + { + } + + virtual void TearDown() + { + } +}; + +TEST_F(KeywordSearchTest, keyword_test_conflict_resolution) +{ + // test to make sure the longest keyword in a chain of conflicting keywords gets chosen + MWDialogue::KeywordSearch search; + search.seed("foo bar", 0); + search.seed("bar lock", 0); + search.seed("lock switch", 0); + + std::string text = "foo bar lock switch"; + + std::vector::Match> matches; + search.highlightKeywords(text.begin(), text.end(), matches); + + // Should contain: "foo bar", "lock switch" + ASSERT_TRUE (matches.size() == 2); + ASSERT_TRUE (std::string(matches.front().mBeg, matches.front().mEnd) == "foo bar"); + ASSERT_TRUE (std::string(matches.rbegin()->mBeg, matches.rbegin()->mEnd) == "lock switch"); +} + +TEST_F(KeywordSearchTest, keyword_test_conflict_resolution2) +{ + MWDialogue::KeywordSearch search; + search.seed("the dwemer", 0); + search.seed("dwemer language", 0); + + std::string text = "the dwemer language"; + + std::vector::Match> matches; + search.highlightKeywords(text.begin(), text.end(), matches); + + ASSERT_TRUE (matches.size() == 1); + ASSERT_TRUE (std::string(matches.front().mBeg, matches.front().mEnd) == "dwemer language"); +} + + +TEST_F(KeywordSearchTest, keyword_test_conflict_resolution3) +{ + // testing that the longest keyword is chosen, rather than maximizing the + // amount of highlighted characters by highlighting the first and last keyword + MWDialogue::KeywordSearch search; + search.seed("foo bar", 0); + search.seed("bar lock", 0); + search.seed("lock so", 0); + + std::string text = "foo bar lock so"; + + std::vector::Match> matches; + search.highlightKeywords(text.begin(), text.end(), matches); + + ASSERT_TRUE (matches.size() == 1); + ASSERT_TRUE (std::string(matches.front().mBeg, matches.front().mEnd) == "bar lock"); +} diff --git a/apps/openmw_test_suite/openmw_test_suite.cpp b/apps/openmw_test_suite/openmw_test_suite.cpp index 81476325e..7cc76b25b 100644 --- a/apps/openmw_test_suite/openmw_test_suite.cpp +++ b/apps/openmw_test_suite/openmw_test_suite.cpp @@ -1,12 +1,6 @@ -#include #include - -int main(int argc, char** argv) { - // The following line causes Google Mock to throw an exception on failure, - // which will be interpreted by your testing framework as a test failure. - ::testing::GTEST_FLAG(throw_on_failure) = false; - ::testing::InitGoogleMock(&argc, argv); - - return RUN_ALL_TESTS(); +GTEST_API_ int main(int argc, char **argv) { + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); } diff --git a/apps/wizard/CMakeLists.txt b/apps/wizard/CMakeLists.txt new file mode 100644 index 000000000..b8cc3fda4 --- /dev/null +++ b/apps/wizard/CMakeLists.txt @@ -0,0 +1,138 @@ +if (WIN32) # windows users can just run the morrowind installer + set(OPENMW_USE_UNSHIELD FALSE) +else() + set(OPENMW_USE_UNSHIELD TRUE) + + find_package(LIBUNSHIELD REQUIRED) + if(NOT LIBUNSHIELD_FOUND) + message(FATAL_ERROR "Failed to find Unshield library") + endif(NOT LIBUNSHIELD_FOUND) +endif() + +set(WIZARD + componentselectionpage.cpp + conclusionpage.cpp + existinginstallationpage.cpp + importpage.cpp + inisettings.cpp + installationtargetpage.cpp + intropage.cpp + languageselectionpage.cpp + main.cpp + mainwizard.cpp + methodselectionpage.cpp + + utils/componentlistwidget.cpp +) + +if(WIN32) + list(APPEND WIZARD ${CMAKE_SOURCE_DIR}/files/windows/openmw-wizard.rc) +endif() + +set(WIZARD_HEADER + componentselectionpage.hpp + conclusionpage.hpp + existinginstallationpage.hpp + importpage.hpp + inisettings.hpp + installationtargetpage.hpp + intropage.hpp + languageselectionpage.hpp + mainwizard.hpp + methodselectionpage.hpp + + utils/componentlistwidget.hpp +) + +# Headers that must be pre-processed +set(WIZARD_HEADER_MOC + componentselectionpage.hpp + conclusionpage.hpp + existinginstallationpage.hpp + importpage.hpp + installationtargetpage.hpp + intropage.hpp + languageselectionpage.hpp + mainwizard.hpp + methodselectionpage.hpp + + utils/componentlistwidget.hpp +) + +set(WIZARD_UI + ${CMAKE_SOURCE_DIR}/files/ui/wizard/componentselectionpage.ui + ${CMAKE_SOURCE_DIR}/files/ui/wizard/conclusionpage.ui + ${CMAKE_SOURCE_DIR}/files/ui/wizard/existinginstallationpage.ui + ${CMAKE_SOURCE_DIR}/files/ui/wizard/importpage.ui + ${CMAKE_SOURCE_DIR}/files/ui/wizard/installationtargetpage.ui + ${CMAKE_SOURCE_DIR}/files/ui/wizard/intropage.ui + ${CMAKE_SOURCE_DIR}/files/ui/wizard/languageselectionpage.ui + ${CMAKE_SOURCE_DIR}/files/ui/wizard/methodselectionpage.ui +) + +if (OPENMW_USE_UNSHIELD) + set (WIZARD ${WIZARD} installationpage.cpp unshield/unshieldworker.cpp) + set (WIZARD_HEADER ${WIZARD_HEADER} installationpage.hpp unshield/unshieldworker.hpp) + set (WIZARD_HEADER_MOC ${WIZARD_HEADER_MOC} installationpage.hpp unshield/unshieldworker.hpp) + set (WIZARD_UI ${WIZARD_UI} ${CMAKE_SOURCE_DIR}/files/ui/wizard/installationpage.ui) + add_definitions(-DOPENMW_USE_UNSHIELD) +endif (OPENMW_USE_UNSHIELD) + + +source_group(wizard FILES ${WIZARD} ${WIZARD_HEADER}) + +find_package(Qt4 REQUIRED) +set(QT_USE_QTGUI 1) + +# Set some platform specific settings +if(WIN32) + set(GUI_TYPE WIN32) + set(QT_USE_QTMAIN TRUE) +endif(WIN32) + +QT4_ADD_RESOURCES(RCC_SRCS ${CMAKE_SOURCE_DIR}/files/wizard/wizard.qrc) +QT4_WRAP_CPP(MOC_SRCS ${WIZARD_HEADER_MOC}) +QT4_WRAP_UI(UI_HDRS ${WIZARD_UI}) + + +include(${QT_USE_FILE}) +include_directories(${CMAKE_CURRENT_BINARY_DIR}) + +if (OPENMW_USE_UNSHIELD) + include_directories(${LIBUNSHIELD_INCLUDE_DIR}) +endif() + +add_executable(openmw-wizard + ${GUI_TYPE} + ${WIZARD} + ${WIZARD_HEADER} + ${RCC_SRCS} + ${MOC_SRCS} + ${UI_HDRS} +) + +target_link_libraries(openmw-wizard + ${Boost_LIBRARIES} + ${QT_LIBRARIES} + components +) + +if (OPENMW_USE_UNSHIELD) + target_link_libraries(openmw-wizard ${LIBUNSHIELD_LIBRARY}) +endif() + + +if(DPKG_PROGRAM) + INSTALL(TARGETS openmw-wizard RUNTIME DESTINATION games COMPONENT openmw-wizard) +endif() + +if (BUILD_WITH_CODE_COVERAGE) + add_definitions (--coverage) + target_link_libraries(openmw-wizard gcov) +endif() + +# Workaround for binutil => 2.23 problem when linking, should be fixed eventually upstream +if (UNIX AND NOT APPLE) +target_link_libraries(openmw-wizard dl Xt) +endif() + diff --git a/apps/wizard/componentselectionpage.cpp b/apps/wizard/componentselectionpage.cpp new file mode 100644 index 000000000..1fcde7857 --- /dev/null +++ b/apps/wizard/componentselectionpage.cpp @@ -0,0 +1,164 @@ +#include "componentselectionpage.hpp" + +#include +#include +#include +#include + +#include "mainwizard.hpp" + +Wizard::ComponentSelectionPage::ComponentSelectionPage(QWidget *parent) : + QWizardPage(parent) +{ + mWizard = qobject_cast(parent); + + setupUi(this); + + setCommitPage(true); + setButtonText(QWizard::CommitButton, tr("&Install")); + + registerField(QLatin1String("installation.components"), componentsList); + + connect(componentsList, SIGNAL(itemChanged(QListWidgetItem *)), + this, SLOT(updateButton(QListWidgetItem *))); + +} + +void Wizard::ComponentSelectionPage::updateButton(QListWidgetItem *item) +{ + if (field(QLatin1String("installation.new")).toBool() == true) + return; // Morrowind is always checked here + + bool unchecked = true; + + for (int i =0; i < componentsList->count(); ++i) { + QListWidgetItem *item = componentsList->item(i); + + if (!item) + continue; + + if (item->checkState() == Qt::Checked) { + unchecked = false; + } + } + + if (unchecked) { + setCommitPage(false); + setButtonText(QWizard::NextButton, tr("&Skip")); + } else { + setCommitPage(true); + } +} + +void Wizard::ComponentSelectionPage::initializePage() +{ + componentsList->clear(); + + QString path(field(QLatin1String("installation.path")).toString()); + + QListWidgetItem *morrowindItem = new QListWidgetItem(QLatin1String("Morrowind")); + QListWidgetItem *tribunalItem = new QListWidgetItem(QLatin1String("Tribunal")); + QListWidgetItem *bloodmoonItem = new QListWidgetItem(QLatin1String("Bloodmoon")); + + if (field(QLatin1String("installation.new")).toBool() == true) + { + morrowindItem->setFlags((morrowindItem->flags() & ~Qt::ItemIsEnabled) | Qt::ItemIsUserCheckable); + morrowindItem->setData(Qt::CheckStateRole, Qt::Checked); + componentsList->addItem(morrowindItem); + + tribunalItem->setFlags(tribunalItem->flags() | Qt::ItemIsUserCheckable); + tribunalItem->setData(Qt::CheckStateRole, Qt::Checked); + componentsList->addItem(tribunalItem); + + bloodmoonItem->setFlags(bloodmoonItem->flags() | Qt::ItemIsUserCheckable); + bloodmoonItem->setData(Qt::CheckStateRole, Qt::Checked); + componentsList->addItem(bloodmoonItem); + } else { + + if (mWizard->mInstallations[path].hasMorrowind) { + morrowindItem->setText(tr("Morrowind\t\t(installed)")); + morrowindItem->setFlags((morrowindItem->flags() & ~Qt::ItemIsEnabled) | Qt::ItemIsUserCheckable); + morrowindItem->setData(Qt::CheckStateRole, Qt::Unchecked); + } else { + morrowindItem->setText(tr("Morrowind")); + morrowindItem->setData(Qt::CheckStateRole, Qt::Checked); + } + + componentsList->addItem(morrowindItem); + + if (mWizard->mInstallations[path].hasTribunal) { + tribunalItem->setText(tr("Tribunal\t\t(installed)")); + tribunalItem->setFlags((tribunalItem->flags() & ~Qt::ItemIsEnabled) | Qt::ItemIsUserCheckable); + tribunalItem->setData(Qt::CheckStateRole, Qt::Unchecked); + } else { + tribunalItem->setText(tr("Tribunal")); + tribunalItem->setData(Qt::CheckStateRole, Qt::Checked); + } + + componentsList->addItem(tribunalItem); + + if (mWizard->mInstallations[path].hasBloodmoon) { + bloodmoonItem->setText(tr("Bloodmoon\t\t(installed)")); + bloodmoonItem->setFlags((bloodmoonItem->flags() & ~Qt::ItemIsEnabled) | Qt::ItemIsUserCheckable); + bloodmoonItem->setData(Qt::CheckStateRole, Qt::Unchecked); + } else { + bloodmoonItem->setText(tr("Bloodmoon")); + bloodmoonItem->setData(Qt::CheckStateRole, Qt::Checked); + } + + componentsList->addItem(bloodmoonItem); + } +} + +bool Wizard::ComponentSelectionPage::validatePage() +{ + QStringList components(field(QLatin1String("installation.components")).toStringList()); + QString path(field(QLatin1String("installation.path")).toString()); + +// qDebug() << components << path << mWizard->mInstallations[path]; + + if (field(QLatin1String("installation.new")).toBool() == false) { + if (components.contains(QLatin1String("Tribunal")) && !components.contains(QLatin1String("Bloodmoon"))) + { + if (mWizard->mInstallations[path].hasBloodmoon) + { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("About to install Tribunal after Bloodmoon")); + msgBox.setIcon(QMessageBox::Information); + msgBox.setStandardButtons(QMessageBox::Cancel); + msgBox.setText(tr("

    You are about to install Tribunal

    \ +

    Bloodmoon is already installed on your computer.

    \ +

    However, it is recommended that you install Tribunal before Bloodmoon.

    \ +

    Would you like to re-install Bloodmoon?

    ")); + + QAbstractButton *reinstallButton = msgBox.addButton(tr("Re-install &Bloodmoon"), QMessageBox::ActionRole); + msgBox.exec(); + + + if (msgBox.clickedButton() == reinstallButton) { + // Force reinstallation + mWizard->mInstallations[path].hasBloodmoon = false; + QList items = componentsList->findItems(QLatin1String("Bloodmoon"), Qt::MatchStartsWith); + + foreach (QListWidgetItem *item, items) { + item->setText(QLatin1String("Bloodmoon")); + item->setCheckState(Qt::Checked); + } + + return true; + } + } + } + } + + return true; +} + +int Wizard::ComponentSelectionPage::nextId() const +{ + if (isCommitPage()) { + return MainWizard::Page_Installation; + } else { + return MainWizard::Page_Import; + } +} diff --git a/apps/wizard/componentselectionpage.hpp b/apps/wizard/componentselectionpage.hpp new file mode 100644 index 000000000..ca4347108 --- /dev/null +++ b/apps/wizard/componentselectionpage.hpp @@ -0,0 +1,34 @@ +#ifndef COMPONENTSELECTIONPAGE_HPP +#define COMPONENTSELECTIONPAGE_HPP + +#include + +#include "ui_componentselectionpage.h" + +namespace Wizard +{ + class MainWizard; + + class ComponentSelectionPage : public QWizardPage, private Ui::ComponentSelectionPage + { + Q_OBJECT + public: + ComponentSelectionPage(QWidget *parent); + + int nextId() const; + virtual bool validatePage(); + + private slots: + void updateButton(QListWidgetItem *item); + + private: + MainWizard *mWizard; + + protected: + void initializePage(); + + }; + +} + +#endif // COMPONENTSELECTIONPAGE_HPP diff --git a/apps/wizard/conclusionpage.cpp b/apps/wizard/conclusionpage.cpp new file mode 100644 index 000000000..87154732a --- /dev/null +++ b/apps/wizard/conclusionpage.cpp @@ -0,0 +1,54 @@ +#include "conclusionpage.hpp" + +#include + +#include "mainwizard.hpp" + +Wizard::ConclusionPage::ConclusionPage(QWidget *parent) : + QWizardPage(parent) +{ + mWizard = qobject_cast(parent); + + setupUi(this); + setPixmap(QWizard::WatermarkPixmap, QPixmap(QLatin1String(":/images/intropage-background.png"))); +} + +void Wizard::ConclusionPage::initializePage() +{ + // Write the path to openmw.cfg + if (field(QLatin1String("installation.new")).toBool() == true) { + QString path(field(QLatin1String("installation.path")).toString()); + mWizard->addInstallation(path); + } + + if (!mWizard->mError) + { + if ((field(QLatin1String("installation.new")).toBool() == true) + || (field(QLatin1String("installation.import-settings")).toBool() == true)) + { + qDebug() << "IMPORT SETTINGS"; + mWizard->runSettingsImporter(); + } + } + + if (!mWizard->mError) + { + if (field(QLatin1String("installation.new")).toBool() == true) + { + textLabel->setText(tr("

    The OpenMW Wizard successfully installed Morrowind on your computer.

    \ +

    Click Finish to close the Wizard.

    ")); + } else { + textLabel->setText(tr("

    The OpenMW Wizard successfully modified your existing Morrowind installation.

    \ +

    Click Finish to close the Wizard.

    ")); + } + } else { + textLabel->setText(tr("

    The OpenMW Wizard failed to install Morrowind on your computer.

    \ +

    Please report any bugs you might have encountered to our \ + bug tracker.
    Make sure to include the installation log.


    ")); + } +} + +int Wizard::ConclusionPage::nextId() const +{ + return -1; +} diff --git a/apps/wizard/conclusionpage.hpp b/apps/wizard/conclusionpage.hpp new file mode 100644 index 000000000..0e9abed72 --- /dev/null +++ b/apps/wizard/conclusionpage.hpp @@ -0,0 +1,30 @@ +#ifndef CONCLUSIONPAGE_HPP +#define CONCLUSIONPAGE_HPP + +#include + +#include "ui_conclusionpage.h" + +namespace Wizard +{ + class MainWizard; + + class ConclusionPage : public QWizardPage, private Ui::ConclusionPage + { + Q_OBJECT + public: + ConclusionPage(QWidget *parent); + + int nextId() const; + + private: + MainWizard *mWizard; + + protected: + void initializePage(); + + }; + +} + +#endif // CONCLUSIONPAGE_HPP diff --git a/apps/wizard/existinginstallationpage.cpp b/apps/wizard/existinginstallationpage.cpp new file mode 100644 index 000000000..83ea20f5a --- /dev/null +++ b/apps/wizard/existinginstallationpage.cpp @@ -0,0 +1,158 @@ +#include "existinginstallationpage.hpp" + +#include +#include +#include +#include +#include + +#include "mainwizard.hpp" + +Wizard::ExistingInstallationPage::ExistingInstallationPage(QWidget *parent) : + QWizardPage(parent) +{ + mWizard = qobject_cast(parent); + + setupUi(this); + + // Add a placeholder item to the list of installations + QListWidgetItem *emptyItem = new QListWidgetItem(tr("No existing installations detected")); + emptyItem->setFlags(Qt::NoItemFlags); + + installationsList->insertItem(0, emptyItem); + +} + +void Wizard::ExistingInstallationPage::initializePage() +{ + // Add the available installation paths + QStringList paths(mWizard->mInstallations.keys()); + + // Hide the default item if there are installations to choose from + if (paths.isEmpty()) { + installationsList->item(0)->setHidden(false); + } else { + installationsList->item(0)->setHidden(true); + } + + foreach (const QString &path, paths) { + QListWidgetItem *item = new QListWidgetItem(path); + + if (installationsList->findItems(path, Qt::MatchExactly).isEmpty()) + installationsList->addItem(item); + } + + connect(installationsList, SIGNAL(currentTextChanged(QString)), + this, SLOT(textChanged(QString))); + + connect(installationsList,SIGNAL(itemSelectionChanged()), + this, SIGNAL(completeChanged())); +} + +bool Wizard::ExistingInstallationPage::validatePage() +{ + // See if Morrowind.ini is detected, if not, ask the user + // It can be missing entirely + // Or failed to be detected due to the target being a symlink + + QString path(field(QLatin1String("installation.path")).toString()); + QFile file(mWizard->mInstallations[path].iniPath); + + if (!file.exists()) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error detecting Morrowind configuration")); + msgBox.setIcon(QMessageBox::Warning); + msgBox.setStandardButtons(QMessageBox::Cancel); + msgBox.setText(QObject::tr("
    Could not find Morrowind.ini

    \ + The Wizard needs to update settings in this file.

    \ + Press \"Browse...\" to specify the location manually.
    ")); + + QAbstractButton *browseButton = + msgBox.addButton(QObject::tr("B&rowse..."), QMessageBox::ActionRole); + + msgBox.exec(); + + QString iniFile; + if (msgBox.clickedButton() == browseButton) { + iniFile = QFileDialog::getOpenFileName( + this, + QObject::tr("Select configuration file"), + QDir::currentPath(), + QString(tr("Morrowind configuration file (*.ini)"))); + } + + if (iniFile.isEmpty()) { + return false; // Cancel was clicked; + } + + // A proper Morrowind.ini was selected, set it + QFileInfo info(iniFile); + mWizard->mInstallations[path].iniPath = info.absoluteFilePath(); + } + + return true; +} + +void Wizard::ExistingInstallationPage::on_browseButton_clicked() +{ + QString selectedFile = QFileDialog::getOpenFileName( + this, + tr("Select master file"), + QDir::currentPath(), + QString(tr("Morrowind master file (*.esm)")), + NULL, + QFileDialog::DontResolveSymlinks); + + if (selectedFile.isEmpty()) + return; + + QFileInfo info(selectedFile); + + if (!info.exists()) + return; + + if (!mWizard->findFiles(QLatin1String("Morrowind"), info.absolutePath())) + return; // No valid Morrowind installation found + + QString path(QDir::toNativeSeparators(info.absolutePath())); + QList items = installationsList->findItems(path, Qt::MatchExactly); + + if (items.isEmpty()) { + // Path is not yet in the list, add it + mWizard->addInstallation(path); + + // Hide the default item + installationsList->item(0)->setHidden(true); + + QListWidgetItem *item = new QListWidgetItem(path); + installationsList->addItem(item); + installationsList->setCurrentItem(item); // Select it too + } else { + installationsList->setCurrentItem(items.first()); + } + + // Update the button + emit completeChanged(); +} + +void Wizard::ExistingInstallationPage::textChanged(const QString &text) +{ + // Set the installation path manually, as registerField doesn't work + // Because it doesn't accept two widgets operating on a single field + if (!text.isEmpty()) + mWizard->setField(QLatin1String("installation.path"), text); +} + +bool Wizard::ExistingInstallationPage::isComplete() const +{ + if (installationsList->selectionModel()->hasSelection()) { + return true; + } else { + return false; + } +} + +int Wizard::ExistingInstallationPage::nextId() const +{ + return MainWizard::Page_LanguageSelection; +} diff --git a/apps/wizard/existinginstallationpage.hpp b/apps/wizard/existinginstallationpage.hpp new file mode 100644 index 000000000..601295464 --- /dev/null +++ b/apps/wizard/existinginstallationpage.hpp @@ -0,0 +1,37 @@ +#ifndef EXISTINGINSTALLATIONPAGE_HPP +#define EXISTINGINSTALLATIONPAGE_HPP + +#include + +#include "ui_existinginstallationpage.h" + +namespace Wizard +{ + class MainWizard; + + class ExistingInstallationPage : public QWizardPage, private Ui::ExistingInstallationPage + { + Q_OBJECT + public: + ExistingInstallationPage(QWidget *parent); + + int nextId() const; + virtual bool isComplete() const; + virtual bool validatePage(); + + private slots: + void on_browseButton_clicked(); + void textChanged(const QString &text); + + + private: + MainWizard *mWizard; + + protected: + void initializePage(); + + }; + +} + +#endif // EXISTINGINSTALLATIONPAGE_HPP diff --git a/apps/wizard/importpage.cpp b/apps/wizard/importpage.cpp new file mode 100644 index 000000000..71c5544e6 --- /dev/null +++ b/apps/wizard/importpage.cpp @@ -0,0 +1,19 @@ +#include "importpage.hpp" + +#include "mainwizard.hpp" + +Wizard::ImportPage::ImportPage(QWidget *parent) : + QWizardPage(parent) +{ + mWizard = qobject_cast(parent); + + setupUi(this); + + registerField(QLatin1String("installation.import-settings"), importCheckBox); + registerField(QLatin1String("installation.import-addons"), addonsCheckBox); +} + +int Wizard::ImportPage::nextId() const +{ + return MainWizard::Page_Conclusion; +} diff --git a/apps/wizard/importpage.hpp b/apps/wizard/importpage.hpp new file mode 100644 index 000000000..386cd59af --- /dev/null +++ b/apps/wizard/importpage.hpp @@ -0,0 +1,27 @@ +#ifndef IMPORTPAGE_HPP +#define IMPORTPAGE_HPP + +#include + +#include "ui_importpage.h" + +namespace Wizard +{ + class MainWizard; + + class ImportPage : public QWizardPage, private Ui::ImportPage + { + Q_OBJECT + public: + ImportPage(QWidget *parent); + + int nextId() const; + + private: + MainWizard *mWizard; + + }; + +} + +#endif // IMPORTPAGE_HPP diff --git a/apps/wizard/inisettings.cpp b/apps/wizard/inisettings.cpp new file mode 100644 index 000000000..3711ba066 --- /dev/null +++ b/apps/wizard/inisettings.cpp @@ -0,0 +1,219 @@ +#include "inisettings.hpp" + +#include + +#include +#include +#include +#include +#include +#include + +Wizard::IniSettings::IniSettings() +{ +} + +Wizard::IniSettings::~IniSettings() +{ +} + +QStringList Wizard::IniSettings::findKeys(const QString &text) +{ + QStringList result; + + foreach (const QString &key, mSettings.keys()) { + + if (key.startsWith(text)) + result << key; + + } + + return result; +} + +bool Wizard::IniSettings::readFile(QTextStream &stream) +{ + // Look for a square bracket, "'\\[" + // that has one or more "not nothing" in it, "([^]]+)" + // and is closed with a square bracket, "\\]" + QRegExp sectionRe(QLatin1String("^\\[([^]]+)\\]")); + + // Find any character(s) that is/are not equal sign(s), "[^=]+" + // followed by an optional whitespace, an equal sign, and another optional whitespace, "\\s*=\\s*" + // and one or more periods, "(.+)" + QRegExp keyRe(QLatin1String("^([^=]+)\\s*=\\s*(.+)$")); + + QString currentSection; + + while (!stream.atEnd()) + { + const QString line(stream.readLine()); + + if (line.isEmpty() || line.startsWith(QLatin1Char(';'))) + continue; + + if (sectionRe.exactMatch(line)) + { + currentSection = sectionRe.cap(1); + } + else if (keyRe.indexIn(line) != -1) + { + QString key = keyRe.cap(1).trimmed(); + QString value = keyRe.cap(2).trimmed(); + + // Append the section, but only if there is one + if (!currentSection.isEmpty()) + key = currentSection + QLatin1Char('/') + key; + + mSettings[key] = QVariant(value); + } + } + + return true; +} + +bool Wizard::IniSettings::writeFile(const QString &path, QTextStream &stream) +{ + // Look for a square bracket, "'\\[" + // that has one or more "not nothing" in it, "([^]]+)" + // and is closed with a square bracket, "\\]" + QRegExp sectionRe(QLatin1String("^\\[([^]]+)\\]")); + + // Find any character(s) that is/are not equal sign(s), "[^=]+" + // followed by an optional whitespace, an equal sign, and another optional whitespace, "\\s*=\\s*" + // and one or more periods, "(.+)" + QRegExp keyRe(QLatin1String("^([^=]+)\\s*=\\s*(.+)$")); + + const QStringList keys(mSettings.keys()); + + QString currentSection; + QString buffer; + + while (!stream.atEnd()) { + + const QString line(stream.readLine()); + + if (line.isEmpty() || line.startsWith(QLatin1Char(';'))) { + buffer.append(line + QLatin1String("\n")); + continue; + } + + if (sectionRe.exactMatch(line)) { + buffer.append(line + QLatin1String("\n")); + currentSection = sectionRe.cap(1); + } else if (keyRe.indexIn(line) != -1) { + QString key(keyRe.cap(1).trimmed()); + QString lookupKey(key); + + // Append the section, but only if there is one + if (!currentSection.isEmpty()) + lookupKey = currentSection + QLatin1Char('/') + key; + + buffer.append(key + QLatin1Char('=') + mSettings[lookupKey].toString() + QLatin1String("\n")); + mSettings.remove(lookupKey); + } + } + + // Add the new settings to the buffer + QHashIterator i(mSettings); + while (i.hasNext()) { + i.next(); + + QStringList fullKey(i.key().split(QLatin1Char('/'))); + QString section(fullKey.at(0)); + section.prepend(QLatin1Char('[')); + section.append(QLatin1Char(']')); + QString key(fullKey.at(1)); + + int index = buffer.lastIndexOf(section); + if (index != -1) { + // Look for the next section + index = buffer.indexOf(QLatin1Char('['), index + 1); + + if (index == -1 ) { + // We are at the last section, append it to the bottom of the file + buffer.append(QString("\n%1=%2").arg(key, i.value().toString())); + mSettings.remove(i.key()); + continue; + } else { + // Not at last section, add the key at the index + buffer.insert(index - 1, QString("\n%1=%2").arg(key, i.value().toString())); + mSettings.remove(i.key()); + } + + } else { + // Add the section to the end of the file, because it's not found + buffer.append(QString("\n%1\n").arg(section)); + i.previous(); + } + } + + // Now we reopen the file, this time we write + QFile file(path); + + if (file.open(QIODevice::ReadWrite | QIODevice::Truncate | QIODevice::Text)) { + QTextStream in(&file); + in.setCodec(stream.codec()); + + // Write the updated buffer to an empty file + in << buffer; + file.flush(); + file.close(); + } else { + return false; + } + + return true; +} + +bool Wizard::IniSettings::parseInx(const QString &path) +{ + QFile file(path); + + if (file.open(QIODevice::ReadOnly)) + { + + const QByteArray data(file.readAll()); + const QByteArray pattern("\x21\x00\x1A\x01\x04\x00\x04\x97\xFF\x06", 10); + + int i = 0; + while ((i = data.indexOf(pattern, i)) != -1) { + + int next = data.indexOf(pattern, i + 1); + if (next == -1) + break; + + QByteArray array(data.mid(i, (next - i))); + + // Skip some invalid entries + if (array.contains("\x04\x96\xFF")) { + ++i; + continue; + } + + // Remove the pattern from the beginning + array.remove(0, 12); + + int index = array.indexOf("\x06"); + const QString section(array.left(index)); + + // Figure how many characters to read for the key + int lenght = array.indexOf("\x06", section.length() + 3) - (section.length() + 3); + const QString key(array.mid(section.length() + 3, lenght)); + + QString value(array.mid(section.length() + key.length() + 6)); + + // Add the value + setValue(section + QLatin1Char('/') + key, QVariant(value)); + + ++i; + } + + file.close(); + } else { + qDebug() << "Failed to open INX file: " << path; + return false; + } + + return true; +} diff --git a/apps/wizard/inisettings.hpp b/apps/wizard/inisettings.hpp new file mode 100644 index 000000000..d425a9b1b --- /dev/null +++ b/apps/wizard/inisettings.hpp @@ -0,0 +1,56 @@ +#ifndef INISETTINGS_HPP +#define INISETTINGS_HPP + +#include +#include + +class QTextStream; + +namespace Wizard +{ + + typedef QHash SettingsMap; + + class IniSettings + { + public: + explicit IniSettings(); + ~IniSettings(); + + inline QVariant value(const QString &key, const QVariant &defaultValue = QVariant()) const + { + return mSettings.value(key, defaultValue); + } + + inline QList values() const + { + return mSettings.values(); + } + + inline void setValue(const QString &key, const QVariant &value) + { + mSettings.insert(key, value); + } + + inline void remove(const QString &key) + { + mSettings.remove(key); + } + + QStringList findKeys(const QString &text); + + bool readFile(QTextStream &stream); + bool writeFile(const QString &path, QTextStream &stream); + + bool parseInx(const QString &path); + + private: + + int getLastNewline(const QString &buffer, int from); + + SettingsMap mSettings; + }; + +} + +#endif // INISETTINGS_HPP diff --git a/apps/wizard/installationpage.cpp b/apps/wizard/installationpage.cpp new file mode 100644 index 000000000..dc2674680 --- /dev/null +++ b/apps/wizard/installationpage.cpp @@ -0,0 +1,244 @@ +#include "installationpage.hpp" + +#include +#include +#include +#include +#include + +#include "mainwizard.hpp" +#include "inisettings.hpp" + +Wizard::InstallationPage::InstallationPage(QWidget *parent) : + QWizardPage(parent) +{ + mWizard = qobject_cast(parent); + + setupUi(this); + + mFinished = false; + + mThread = new QThread(); + mUnshield = new UnshieldWorker(); + mUnshield->moveToThread(mThread); + + connect(mThread, SIGNAL(started()), + mUnshield, SLOT(extract())); + + connect(mUnshield, SIGNAL(finished()), + mThread, SLOT(quit())); + + connect(mUnshield, SIGNAL(finished()), + mUnshield, SLOT(deleteLater())); + + connect(mUnshield, SIGNAL(finished()), + mThread, SLOT(deleteLater()));; + + connect(mUnshield, SIGNAL(finished()), + this, SLOT(installationFinished()), Qt::QueuedConnection); + + connect(mUnshield, SIGNAL(error(QString, QString)), + this, SLOT(installationError(QString, QString)), Qt::QueuedConnection); + + connect(mUnshield, SIGNAL(textChanged(QString)), + installProgressLabel, SLOT(setText(QString)), Qt::QueuedConnection); + + connect(mUnshield, SIGNAL(textChanged(QString)), + logTextEdit, SLOT(appendPlainText(QString)), Qt::QueuedConnection); + + connect(mUnshield, SIGNAL(textChanged(QString)), + mWizard, SLOT(addLogText(QString)), Qt::QueuedConnection); + + connect(mUnshield, SIGNAL(progressChanged(int)), + installProgressBar, SLOT(setValue(int)), Qt::QueuedConnection); + + connect(mUnshield, SIGNAL(requestFileDialog(Wizard::Component)), + this, SLOT(showFileDialog(Wizard::Component)), Qt::QueuedConnection); +} + +Wizard::InstallationPage::~InstallationPage() +{ + if (mThread->isRunning()) { + mUnshield->stopWorker(); + mThread->wait(); + } + + delete mUnshield; + delete mThread; +} + +void Wizard::InstallationPage::initializePage() +{ + QString path(field(QLatin1String("installation.path")).toString()); + QStringList components(field(QLatin1String("installation.components")).toStringList()); + + logTextEdit->appendPlainText(QString("Installing to %1").arg(path)); + logTextEdit->appendPlainText(QString("Installing %1.").arg(components.join(", "))); + + installProgressBar->setMinimum(0); + + // Set the progressbar maximum to a multiple of 100 + // That way installing all three components would yield 300% + // When one component is done the bar will be filled by 33% + + if (field(QLatin1String("installation.new")).toBool() == true) { + installProgressBar->setMaximum((components.count() * 100)); + } else { + if (components.contains(QLatin1String("Tribunal")) + && !mWizard->mInstallations[path].hasTribunal) + installProgressBar->setMaximum(100); + + if (components.contains(QLatin1String("Bloodmoon")) + && !mWizard->mInstallations[path].hasBloodmoon) + installProgressBar->setMaximum(installProgressBar->maximum() + 100); + } + + startInstallation(); +} + +void Wizard::InstallationPage::startInstallation() +{ + QStringList components(field(QLatin1String("installation.components")).toStringList()); + QString path(field(QLatin1String("installation.path")).toString()); + + if (field(QLatin1String("installation.new")).toBool() == true) + { + // Always install Morrowind + mUnshield->setInstallComponent(Wizard::Component_Morrowind, true); + + if (components.contains(QLatin1String("Tribunal"))) + mUnshield->setInstallComponent(Wizard::Component_Tribunal, true); + + if (components.contains(QLatin1String("Bloodmoon"))) + mUnshield->setInstallComponent(Wizard::Component_Bloodmoon, true); + } else { + // Morrowind should already be installed + mUnshield->setInstallComponent(Wizard::Component_Morrowind, false); + + if (components.contains(QLatin1String("Tribunal")) + && !mWizard->mInstallations[path].hasTribunal) + mUnshield->setInstallComponent(Wizard::Component_Tribunal, true); + + if (components.contains(QLatin1String("Bloodmoon")) + && !mWizard->mInstallations[path].hasBloodmoon) + mUnshield->setInstallComponent(Wizard::Component_Bloodmoon, true); + + // Set the location of the Morrowind.ini to update + mUnshield->setIniPath(mWizard->mInstallations[path].iniPath); + mUnshield->setupSettings(); + } + + // Set the installation target path + mUnshield->setPath(path); + + // Set the right codec to use for Morrowind.ini + QString language(field(QLatin1String("installation.language")).toString()); + + if (language == QLatin1String("Polish")) { + mUnshield->setIniCodec(QTextCodec::codecForName("windows-1250")); + } else if (language == QLatin1String("Russian")) { + mUnshield->setIniCodec(QTextCodec::codecForName("windows-1251")); + } else { + mUnshield->setIniCodec(QTextCodec::codecForName("windows-1252")); + } + + mThread->start(); +} + +void Wizard::InstallationPage::showFileDialog(Wizard::Component component) +{ + QString name; + switch (component) { + + case Wizard::Component_Morrowind: + name = QLatin1String("Morrowind"); + break; + case Wizard::Component_Tribunal: + name = QLatin1String("Tribunal"); + break; + case Wizard::Component_Bloodmoon: + name = QLatin1String("Bloodmoon"); + break; + } + + QString path = QFileDialog::getExistingDirectory(this, + tr("Select %1 installation media").arg(name), + QDir::rootPath()); + + if (path.isEmpty()) { + logTextEdit->appendHtml(tr("


    \ + Error: The installation was aborted by the user

    ")); + + mWizard->addLogText(QLatin1String("Error: The installation was aborted by the user")); + mWizard->mError = true; + + emit completeChanged(); + return; + } + + mUnshield->setDiskPath(path); +} + +void Wizard::InstallationPage::installationFinished() +{ + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Installation finished")); + msgBox.setIcon(QMessageBox::Information); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("Installation completed successfully!")); + + msgBox.exec(); + + mFinished = true; + emit completeChanged(); +} + +void Wizard::InstallationPage::installationError(const QString &text, const QString &details) +{ + installProgressLabel->setText(tr("Installation failed!")); + + logTextEdit->appendHtml(tr("


    \ + Error: %1

    ").arg(text)); + logTextEdit->appendHtml(tr("

    \ + %1

    ").arg(details)); + + mWizard->addLogText(QLatin1String("Error: ") + text); + mWizard->addLogText(details); + + mWizard->mError = true; + QMessageBox msgBox; + msgBox.setWindowTitle(tr("An error occurred")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

    The Wizard has encountered an error

    \ +

    The error reported was:

    %1

    \ +

    Press "Show Details..." for more information.

    ").arg(text)); + + msgBox.setDetailedText(details); + msgBox.exec(); + + + emit completeChanged(); +} + +bool Wizard::InstallationPage::isComplete() const +{ + if (!mWizard->mError) { + return mFinished; + } else { + return true; + } +} + +int Wizard::InstallationPage::nextId() const +{ + if (field(QLatin1String("installation.new")).toBool() == true) { + return MainWizard::Page_Conclusion; + } else { + if (!mWizard->mError) { + return MainWizard::Page_Import; + } else { + return MainWizard::Page_Conclusion; + } + } +} diff --git a/apps/wizard/installationpage.hpp b/apps/wizard/installationpage.hpp new file mode 100644 index 000000000..822cd21cd --- /dev/null +++ b/apps/wizard/installationpage.hpp @@ -0,0 +1,50 @@ +#ifndef INSTALLATIONPAGE_HPP +#define INSTALLATIONPAGE_HPP + +#include + +#include "unshield/unshieldworker.hpp" +#include "ui_installationpage.h" +#include "inisettings.hpp" + +class QThread; + +namespace Wizard +{ + class MainWizard; + class IniSettings; + class UnshieldWorker; + + class InstallationPage : public QWizardPage, private Ui::InstallationPage + { + Q_OBJECT + public: + InstallationPage(QWidget *parent); + ~InstallationPage(); + + int nextId() const; + virtual bool isComplete() const; + + private: + MainWizard *mWizard; + bool mFinished; + + QThread* mThread; + UnshieldWorker *mUnshield; + + void startInstallation(); + + private slots: + void showFileDialog(Wizard::Component component); + + void installationFinished(); + void installationError(const QString &text, const QString &details); + + protected: + void initializePage(); + + }; + +} + +#endif // INSTALLATIONPAGE_HPP diff --git a/apps/wizard/installationtargetpage.cpp b/apps/wizard/installationtargetpage.cpp new file mode 100644 index 000000000..3a179a2cb --- /dev/null +++ b/apps/wizard/installationtargetpage.cpp @@ -0,0 +1,101 @@ +#include "installationtargetpage.hpp" + +#include +#include +#include + +#include "mainwizard.hpp" + +Wizard::InstallationTargetPage::InstallationTargetPage(QWidget *parent, const Files::ConfigurationManager &cfg) : + QWizardPage(parent), + mCfgMgr(cfg) +{ + mWizard = qobject_cast(parent); + + setupUi(this); + + registerField(QLatin1String("installation.path*"), targetLineEdit); +} + +void Wizard::InstallationTargetPage::initializePage() +{ + QString path(QFile::decodeName(mCfgMgr.getUserDataPath().string().c_str())); + path.append(QDir::separator() + QLatin1String("data")); + + QDir dir(path); + targetLineEdit->setText(QDir::toNativeSeparators(dir.absolutePath())); +} + +bool Wizard::InstallationTargetPage::validatePage() +{ + QString path(field(QLatin1String("installation.path")).toString()); + + qDebug() << "Validating path: " << path; + + if (!QFile::exists(path)) { + QDir dir; + + if (!dir.mkpath(path)) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error creating destination")); + msgBox.setIcon(QMessageBox::Warning); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

    Could not create the destination directory

    \ +

    Please make sure you have the right permissions \ + and try again, or specify a different location.

    ")); + msgBox.exec(); + return false; + } + } + + QFileInfo info(path); + + if (!info.isWritable()) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Insufficient permissions")); + msgBox.setIcon(QMessageBox::Warning); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

    Could not write to the destination directory

    \ +

    Please make sure you have the right permissions \ + and try again, or specify a different location.

    ")); + msgBox.exec(); + return false; + } + + if (mWizard->findFiles(QLatin1String("Morrowind"), path)) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Destination not empty")); + msgBox.setIcon(QMessageBox::Warning); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

    The destination directory is not empty

    \ +

    An existing Morrowind installation is present in the specified location.

    \ +

    Please specify a different location, or go back and select the location as an existing installation.

    ")); + msgBox.exec(); + return false; + } + + return true; +} + +void Wizard::InstallationTargetPage::on_browseButton_clicked() +{ + QString selectedPath = QFileDialog::getExistingDirectory( + this, + tr("Select where to install Morrowind"), + QDir::homePath(), + QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); + + qDebug() << selectedPath; + QFileInfo info(selectedPath); + if (!info.exists()) + return; + + if (info.isWritable()) + targetLineEdit->setText(info.absoluteFilePath()); + +} + +int Wizard::InstallationTargetPage::nextId() const +{ + return MainWizard::Page_LanguageSelection; +} diff --git a/apps/wizard/installationtargetpage.hpp b/apps/wizard/installationtargetpage.hpp new file mode 100644 index 000000000..ca3b505b7 --- /dev/null +++ b/apps/wizard/installationtargetpage.hpp @@ -0,0 +1,40 @@ +#ifndef INSTALLATIONTARGETPAGE_HPP +#define INSTALLATIONTARGETPAGE_HPP + +#include + +#include "ui_installationtargetpage.h" + +namespace Files +{ + struct ConfigurationManager; +} + +namespace Wizard +{ + class MainWizard; + + class InstallationTargetPage : public QWizardPage, private Ui::InstallationTargetPage + { + Q_OBJECT + public: + InstallationTargetPage(QWidget *parent, const Files::ConfigurationManager &cfg); + + int nextId() const; + virtual bool validatePage(); + + private slots: + void on_browseButton_clicked(); + + private: + MainWizard *mWizard; + const Files::ConfigurationManager &mCfgMgr; + + protected: + void initializePage(); + + }; + +} + +#endif // INSTALLATIONTARGETPAGE_HPP diff --git a/apps/wizard/intropage.cpp b/apps/wizard/intropage.cpp new file mode 100644 index 000000000..0a98ae5f3 --- /dev/null +++ b/apps/wizard/intropage.cpp @@ -0,0 +1,17 @@ +#include "intropage.hpp" + +#include "mainwizard.hpp" + +Wizard::IntroPage::IntroPage(QWidget *parent) : + QWizardPage(parent) +{ + mWizard = qobject_cast(parent); + + setupUi(this); + setPixmap(QWizard::WatermarkPixmap, QPixmap(QLatin1String(":/images/intropage-background.png"))); +} + +int Wizard::IntroPage::nextId() const +{ + return MainWizard::Page_MethodSelection; +} diff --git a/apps/wizard/intropage.hpp b/apps/wizard/intropage.hpp new file mode 100644 index 000000000..4ad9b4111 --- /dev/null +++ b/apps/wizard/intropage.hpp @@ -0,0 +1,26 @@ +#ifndef INTROPAGE_HPP +#define INTROPAGE_HPP + +#include + +#include "ui_intropage.h" + +namespace Wizard +{ + class MainWizard; + + class IntroPage : public QWizardPage, private Ui::IntroPage + { + Q_OBJECT + public: + IntroPage(QWidget *parent); + + int nextId() const; + + private: + MainWizard *mWizard; + }; + +} + +#endif // INTROPAGE_HPP diff --git a/apps/wizard/languageselectionpage.cpp b/apps/wizard/languageselectionpage.cpp new file mode 100644 index 000000000..0d5132f5b --- /dev/null +++ b/apps/wizard/languageselectionpage.cpp @@ -0,0 +1,51 @@ +#include "languageselectionpage.hpp" + +#include "mainwizard.hpp" + +#include + +Wizard::LanguageSelectionPage::LanguageSelectionPage(QWidget *parent) : + QWizardPage(parent) +{ + mWizard = qobject_cast(parent); + + setupUi(this); + + registerField(QLatin1String("installation.language"), languageComboBox); +} + +void Wizard::LanguageSelectionPage::initializePage() +{ + QStringList languages; + languages << QLatin1String("English") + << QLatin1String("French") + << QLatin1String("German") + << QLatin1String("Italian") + << QLatin1String("Polish") + << QLatin1String("Russian") + << QLatin1String("Spanish"); + + languageComboBox->addItems(languages); +} + +int Wizard::LanguageSelectionPage::nextId() const +{ + if (field(QLatin1String("installation.new")).toBool() == true) { + return MainWizard::Page_ComponentSelection; + } else { + QString path(field(QLatin1String("installation.path")).toString()); + + if (path.isEmpty()) + return MainWizard::Page_ComponentSelection; + + // Check if we have to install something + if (mWizard->mInstallations[path].hasMorrowind == true && + mWizard->mInstallations[path].hasTribunal == true && + mWizard->mInstallations[path].hasBloodmoon == true) + { + return MainWizard::Page_Import; + } else { + return MainWizard::Page_ComponentSelection; + } + } +} diff --git a/apps/wizard/languageselectionpage.hpp b/apps/wizard/languageselectionpage.hpp new file mode 100644 index 000000000..abc3edb98 --- /dev/null +++ b/apps/wizard/languageselectionpage.hpp @@ -0,0 +1,28 @@ +#ifndef LANGUAGESELECTIONPAGE_HPP +#define LANGUAGESELECTIONPAGE_HPP + +#include + +#include "ui_languageselectionpage.h" + +namespace Wizard +{ + class MainWizard; + + class LanguageSelectionPage : public QWizardPage, private Ui::LanguageSelectionPage + { + Q_OBJECT + public: + LanguageSelectionPage(QWidget *parent); + + int nextId() const; + + private: + MainWizard *mWizard; + + protected: + void initializePage(); + }; +} + +#endif // LANGUAGESELECTIONPAGE_HPP diff --git a/apps/wizard/main.cpp b/apps/wizard/main.cpp new file mode 100644 index 000000000..e6a94118a --- /dev/null +++ b/apps/wizard/main.cpp @@ -0,0 +1,48 @@ +#include +#include +#include +#include + +#include "mainwizard.hpp" + +#ifdef MAC_OS_X_VERSION_MIN_REQUIRED +#undef MAC_OS_X_VERSION_MIN_REQUIRED +// We need to do this because of Qt: https://bugreports.qt-project.org/browse/QTBUG-22154 +#define MAC_OS_X_VERSION_MIN_REQUIRED __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ +#endif // MAC_OS_X_VERSION_MIN_REQUIRED + +int main(int argc, char *argv[]) +{ + + QApplication app(argc, argv); + + // Now we make sure the current dir is set to application path + QDir dir(QCoreApplication::applicationDirPath()); + + #ifdef Q_OS_MAC + if (dir.dirName() == "MacOS") { + dir.cdUp(); + dir.cdUp(); + dir.cdUp(); + } + + // force Qt to load only LOCAL plugins, don't touch system Qt installation + QDir pluginsPath(QCoreApplication::applicationDirPath()); + pluginsPath.cdUp(); + pluginsPath.cd("Plugins"); + + QStringList libraryPaths; + libraryPaths << pluginsPath.path() << QCoreApplication::applicationDirPath(); + app.setLibraryPaths(libraryPaths); + #endif + + QDir::setCurrent(dir.absolutePath()); + + // Support non-latin characters + QTextCodec::setCodecForCStrings(QTextCodec::codecForName("UTF-8")); + + Wizard::MainWizard wizard; + + wizard.show(); + return app.exec(); +} diff --git a/apps/wizard/mainwizard.cpp b/apps/wizard/mainwizard.cpp new file mode 100644 index 000000000..a1370b125 --- /dev/null +++ b/apps/wizard/mainwizard.cpp @@ -0,0 +1,462 @@ +#include "mainwizard.hpp" + +#include + +#include +#include +#include +#include +#include +#include + +#include "intropage.hpp" +#include "methodselectionpage.hpp" +#include "languageselectionpage.hpp" +#include "existinginstallationpage.hpp" +#include "installationtargetpage.hpp" +#include "componentselectionpage.hpp" +#include "importpage.hpp" +#include "conclusionpage.hpp" + +#ifdef OPENMW_USE_UNSHIELD +#include "installationpage.hpp" +#endif + +using namespace Process; + +Wizard::MainWizard::MainWizard(QWidget *parent) : + mGameSettings(mCfgMgr), + QWizard(parent), + mError(false), + mInstallations() +{ +#ifndef Q_OS_MAC + setWizardStyle(QWizard::ModernStyle); +#else + setWizardStyle(QWizard::ClassicStyle); +#endif + + setWindowTitle(tr("OpenMW Wizard")); + setWindowIcon(QIcon(QLatin1String(":/images/openmw-wizard.png"))); + setMinimumWidth(550); + + // Set the property for comboboxes to the text instead of index + setDefaultProperty("QComboBox", "currentText", "currentIndexChanged"); + + setDefaultProperty("ComponentListWidget", "mCheckedItems", "checkedItemsChanged"); + + mImporterInvoker = new ProcessInvoker(); + + connect(mImporterInvoker->getProcess(), SIGNAL(started()), + this, SLOT(importerStarted())); + + connect(mImporterInvoker->getProcess(), SIGNAL(finished(int,QProcess::ExitStatus)), + this, SLOT(importerFinished(int,QProcess::ExitStatus))); + + mLogError = tr("

    Could not open %1 for writing

    \ +

    Please make sure you have the right permissions \ + and try again.

    "); + + setupLog(); + setupGameSettings(); + setupLauncherSettings(); + setupInstallations(); + setupPages(); +} + +Wizard::MainWizard::~MainWizard() +{ + delete mImporterInvoker; +} + +void Wizard::MainWizard::setupLog() +{ + QString logPath(QString::fromUtf8(mCfgMgr.getLogPath().string().c_str())); + logPath.append(QLatin1String("wizard.log")); + + QFile file(logPath); + + if (!file.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error opening Wizard log file")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(mLogError.arg(file.fileName())); + msgBox.exec(); + return qApp->quit(); + } + + addLogText(QString("Started OpenMW Wizard on %1").arg(QDateTime::currentDateTime().toString())); + + qDebug() << logPath; +} + +void Wizard::MainWizard::addLogText(const QString &text) +{ + QString logPath(QString::fromUtf8(mCfgMgr.getLogPath().string().c_str())); + logPath.append(QLatin1String("wizard.log")); + + QFile file(logPath); + + if (!file.open(QIODevice::ReadWrite | QIODevice::Text)) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error opening Wizard log file")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(mLogError.arg(file.fileName())); + msgBox.exec(); + return qApp->quit(); + } + + if (!file.isSequential()) + file.seek(file.size()); + + QTextStream out(&file); + + if (!text.isEmpty()) + out << text << endl; + +// file.close(); +} + +void Wizard::MainWizard::setupGameSettings() +{ + QString userPath(QString::fromUtf8(mCfgMgr.getUserConfigPath().string().c_str())); + QString globalPath(QString::fromUtf8(mCfgMgr.getGlobalPath().string().c_str())); + QString message(tr("

    Could not open %1 for reading

    \ +

    Please make sure you have the right permissions \ + and try again.

    ")); + + // Load the user config file first, separately + // So we can write it properly, uncontaminated + QString path(userPath + QLatin1String("openmw.cfg")); + QFile file(path); + + qDebug() << "Loading config file:" << qPrintable(path); + + if (file.exists()) { + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error opening OpenMW configuration file")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(message.arg(file.fileName())); + msgBox.exec(); + return qApp->quit(); + } + QTextStream stream(&file); + stream.setCodec(QTextCodec::codecForName("UTF-8")); + + mGameSettings.readUserFile(stream); + } + + // Now the rest + QStringList paths; + paths.append(userPath + QLatin1String("openmw.cfg")); + paths.append(QLatin1String("openmw.cfg")); + paths.append(globalPath + QLatin1String("openmw.cfg")); + + foreach (const QString &path, paths) { + qDebug() << "Loading config file:" << qPrintable(path); + + QFile file(path); + if (file.exists()) { + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error opening OpenMW configuration file")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(message.arg(file.fileName())); + + return qApp->quit(); + } + QTextStream stream(&file); + stream.setCodec(QTextCodec::codecForName("UTF-8")); + + mGameSettings.readFile(stream); + } + file.close(); + } +} + +void Wizard::MainWizard::setupLauncherSettings() +{ + QString path(QString::fromUtf8(mCfgMgr.getUserConfigPath().string().c_str())); + path.append(QLatin1String(Config::LauncherSettings::sLauncherConfigFileName)); + + QString message(tr("

    Could not open %1 for reading

    \ +

    Please make sure you have the right permissions \ + and try again.

    ")); + + + QFile file(path); + + qDebug() << "Loading config file:" << qPrintable(path); + + if (file.exists()) { + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error opening OpenMW configuration file")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(message.arg(file.fileName())); + msgBox.exec(); + return qApp->quit(); + } + QTextStream stream(&file); + stream.setCodec(QTextCodec::codecForName("UTF-8")); + + mLauncherSettings.readFile(stream); + } + + file.close(); + +} + +void Wizard::MainWizard::setupInstallations() +{ + // Check if the paths actually contain a Morrowind installation + foreach (const QString path, mGameSettings.getDataDirs()) { + + if (findFiles(QLatin1String("Morrowind"), path)) + addInstallation(path); + } +} + +void Wizard::MainWizard::runSettingsImporter() +{ + QString path(field(QLatin1String("installation.path")).toString()); + + // Create the file if it doesn't already exist, else the importer will fail + QString userPath(QString::fromUtf8(mCfgMgr.getUserConfigPath().string().c_str())); + QFile file(userPath + QLatin1String("openmw.cfg")); + + if (!file.exists()) { + if (!file.open(QIODevice::ReadWrite)) { + // File cannot be created + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error writing OpenMW configuration file")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

    Could not open or create %1 for writing

    \ +

    Please make sure you have the right permissions \ + and try again.

    ").arg(file.fileName())); + msgBox.exec(); + return qApp->quit(); + } + + file.close(); + } + + // Construct the arguments to run the importer + QStringList arguments; + + // Import plugin selection? + if (field(QLatin1String("installation.new")).toBool() == true + || field(QLatin1String("installation.import-addons")).toBool() == true) + arguments.append(QLatin1String("--game-files")); + + arguments.append(QLatin1String("--encoding")); + + // Set encoding + QString language(field(QLatin1String("installation.language")).toString()); + + if (language == QLatin1String("Polish")) { + arguments.append(QLatin1String("win1250")); + } else if (language == QLatin1String("Russian")) { + arguments.append(QLatin1String("win1251")); + } else { + arguments.append(QLatin1String("win1252")); + } + + // Now the paths + arguments.append(QLatin1String("--ini")); + + if (field(QLatin1String("installation.new")).toBool() == true) { + arguments.append(path + QDir::separator() + QLatin1String("Morrowind.ini")); + } else { + arguments.append(mInstallations[path].iniPath); + } + + arguments.append(QLatin1String("--cfg")); + arguments.append(userPath + QLatin1String("openmw.cfg")); + + if (!mImporterInvoker->startProcess(QLatin1String("openmw-iniimporter"), arguments, false)) + return qApp->quit(); +} + +void Wizard::MainWizard::addInstallation(const QString &path) +{ + qDebug() << "add installation in: " << path; + Installation install;// = new Installation(); + + install.hasMorrowind = findFiles(QLatin1String("Morrowind"), path); + install.hasTribunal = findFiles(QLatin1String("Tribunal"), path); + install.hasBloodmoon = findFiles(QLatin1String("Bloodmoon"), path); + + // Try to autodetect the Morrowind.ini location + QDir dir(path); + QFile file(dir.filePath("Morrowind.ini")); + + // Try the parent directory + // In normal Morrowind installations that's where Morrowind.ini is + if (!file.exists()) { + dir.cdUp(); + file.setFileName(dir.filePath(QLatin1String("Morrowind.ini"))); + } + + if (file.exists()) + install.iniPath = file.fileName(); + + mInstallations.insert(QDir::toNativeSeparators(path), install); + + // Add it to the openmw.cfg too + if (!mGameSettings.getDataDirs().contains(path)) { + mGameSettings.setMultiValue(QLatin1String("data"), path); + mGameSettings.addDataDir(path); + } +} + +void Wizard::MainWizard::setupPages() +{ + setPage(Page_Intro, new IntroPage(this)); + setPage(Page_MethodSelection, new MethodSelectionPage(this)); + setPage(Page_LanguageSelection, new LanguageSelectionPage(this)); + setPage(Page_ExistingInstallation, new ExistingInstallationPage(this)); + setPage(Page_InstallationTarget, new InstallationTargetPage(this, mCfgMgr)); + setPage(Page_ComponentSelection, new ComponentSelectionPage(this)); +#ifdef OPENMW_USE_UNSHIELD + setPage(Page_Installation, new InstallationPage(this)); +#endif + setPage(Page_Import, new ImportPage(this)); + setPage(Page_Conclusion, new ConclusionPage(this)); + setStartId(Page_Intro); + +} + +void Wizard::MainWizard::importerStarted() +{ +} + +void Wizard::MainWizard::importerFinished(int exitCode, QProcess::ExitStatus exitStatus) +{ + if (exitCode != 0 || exitStatus == QProcess::CrashExit) + return; + + // Re-read the settings + setupGameSettings(); +} + +void Wizard::MainWizard::accept() +{ + writeSettings(); + QWizard::accept(); +} + +void Wizard::MainWizard::reject() +{ + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Quit Wizard")); + msgBox.setIcon(QMessageBox::Question); + msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); + msgBox.setText(tr("Are you sure you want to exit the Wizard?")); + + if (msgBox.exec() == QMessageBox::Yes) { + QWizard::reject(); + } +} + +void Wizard::MainWizard::writeSettings() +{ + // Write the encoding and language settings + QString language(field(QLatin1String("installation.language")).toString()); + mLauncherSettings.setValue(QLatin1String("Settings/language"), language); + + if (language == QLatin1String("Polish")) { + mGameSettings.setValue(QLatin1String("encoding"), QLatin1String("win1250")); + } else if (language == QLatin1String("Russian")) { + mGameSettings.setValue(QLatin1String("encoding"), QLatin1String("win1251")); + } else { + mGameSettings.setValue(QLatin1String("encoding"), QLatin1String("win1252")); + } + + // Write the installation path so that openmw can find them + QString path(field(QLatin1String("installation.path")).toString()); + + // Make sure the installation path is the last data= entry + mGameSettings.removeDataDir(path); + mGameSettings.addDataDir(path); + + QString userPath(QString::fromUtf8(mCfgMgr.getUserConfigPath().string().c_str())); + QDir dir(userPath); + + if (!dir.exists()) { + if (!dir.mkpath(userPath)) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error creating OpenMW configuration directory")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

    Could not create %1

    \ +

    Please make sure you have the right permissions \ + and try again.

    ").arg(userPath)); + msgBox.exec(); + return qApp->quit(); + } + } + + // Game settings + QFile file(userPath + QLatin1String("openmw.cfg")); + + if (!file.open(QIODevice::ReadWrite | QIODevice::Text | QIODevice::Truncate)) { + // File cannot be opened or created + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error writing OpenMW configuration file")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

    Could not open %1 for writing

    \ +

    Please make sure you have the right permissions \ + and try again.

    ").arg(file.fileName())); + msgBox.exec(); + return qApp->quit(); + } + + QTextStream stream(&file); + stream.setCodec(QTextCodec::codecForName("UTF-8")); + + mGameSettings.writeFile(stream); + file.close(); + + // Launcher settings + file.setFileName(userPath + QLatin1String(Config::LauncherSettings::sLauncherConfigFileName)); + + if (!file.open(QIODevice::ReadWrite | QIODevice::Text | QIODevice::Truncate)) { + // File cannot be opened or created + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error writing OpenMW configuration file")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

    Could not open %1 for writing

    \ +

    Please make sure you have the right permissions \ + and try again.

    ").arg(file.fileName())); + msgBox.exec(); + return qApp->quit(); + } + + stream.setDevice(&file); + stream.setCodec(QTextCodec::codecForName("UTF-8")); + + mLauncherSettings.writeFile(stream); + file.close(); +} + +bool Wizard::MainWizard::findFiles(const QString &name, const QString &path) +{ + QDir dir(path); + + if (!dir.exists()) + return false; + + // TODO: add MIME handling to make sure the files are real + return (dir.entryList().contains(name + QLatin1String(".esm"), Qt::CaseInsensitive) + && dir.entryList().contains(name + QLatin1String(".bsa"), Qt::CaseInsensitive)); +} diff --git a/apps/wizard/mainwizard.hpp b/apps/wizard/mainwizard.hpp new file mode 100644 index 000000000..c22f20c25 --- /dev/null +++ b/apps/wizard/mainwizard.hpp @@ -0,0 +1,88 @@ +#ifndef MAINWIZARD_HPP +#define MAINWIZARD_HPP + +#include +#include +#include + +#include + +#ifndef Q_MOC_RUN +#include +#endif +#include +#include + +namespace Wizard +{ + class MainWizard : public QWizard + { + Q_OBJECT + + public: + struct Installation { + bool hasMorrowind; + bool hasTribunal; + bool hasBloodmoon; + + QString iniPath; + }; + + enum { + Page_Intro, + Page_MethodSelection, + Page_LanguageSelection, + Page_ExistingInstallation, + Page_InstallationTarget, + Page_ComponentSelection, + Page_Installation, + Page_Import, + Page_Conclusion + }; + + MainWizard(QWidget *parent = 0); + ~MainWizard(); + + bool findFiles(const QString &name, const QString &path); + void addInstallation(const QString &path); + void runSettingsImporter(); + + QMap mInstallations; + + Files::ConfigurationManager mCfgMgr; + + Process::ProcessInvoker *mImporterInvoker; + + bool mError; + + public slots: + void addLogText(const QString &text); + + private: + + void setupLog(); + void setupGameSettings(); + void setupLauncherSettings(); + void setupInstallations(); + void setupPages(); + + void writeSettings(); + + Config::GameSettings mGameSettings; + Config::LauncherSettings mLauncherSettings; + + QString mLogError; + + private slots: + + void importerStarted(); + void importerFinished(int exitCode, QProcess::ExitStatus exitStatus); + + void accept(); + void reject(); + + }; + +} + +#endif // MAINWIZARD_HPP diff --git a/apps/wizard/methodselectionpage.cpp b/apps/wizard/methodselectionpage.cpp new file mode 100644 index 000000000..5f3917bd5 --- /dev/null +++ b/apps/wizard/methodselectionpage.cpp @@ -0,0 +1,27 @@ +#include "methodselectionpage.hpp" +#include +#include "mainwizard.hpp" + +Wizard::MethodSelectionPage::MethodSelectionPage(QWidget *parent) : + QWizardPage(parent) +{ + mWizard = qobject_cast(parent); + + setupUi(this); + +#ifndef OPENMW_USE_UNSHIELD + newLocationRadioButton->setEnabled(false); + existingLocationRadioButton->setChecked(true); +#endif + + registerField(QLatin1String("installation.new"), newLocationRadioButton); +} + +int Wizard::MethodSelectionPage::nextId() const +{ + if (field(QLatin1String("installation.new")).toBool() == true) { + return MainWizard::Page_InstallationTarget; + } else { + return MainWizard::Page_ExistingInstallation; + } +} diff --git a/apps/wizard/methodselectionpage.hpp b/apps/wizard/methodselectionpage.hpp new file mode 100644 index 000000000..60941c651 --- /dev/null +++ b/apps/wizard/methodselectionpage.hpp @@ -0,0 +1,27 @@ +#ifndef METHODSELECTIONPAGE_HPP +#define METHODSELECTIONPAGE_HPP + +#include + +#include "ui_methodselectionpage.h" + +namespace Wizard +{ + class MainWizard; + + class MethodSelectionPage : public QWizardPage, private Ui::MethodSelectionPage + { + Q_OBJECT + public: + MethodSelectionPage(QWidget *parent); + + int nextId() const; + + private: + MainWizard *mWizard; + + }; + +} + +#endif // METHODSELECTIONPAGE_HPP diff --git a/apps/wizard/unshield/unshieldworker.cpp b/apps/wizard/unshield/unshieldworker.cpp new file mode 100644 index 000000000..11e090ed1 --- /dev/null +++ b/apps/wizard/unshield/unshieldworker.cpp @@ -0,0 +1,924 @@ +#include "unshieldworker.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +Wizard::UnshieldWorker::UnshieldWorker(QObject *parent) : + QObject(parent), + mIniSettings() +{ + unshield_set_log_level(0); + + mPath = QString(); + mIniPath = QString(); + mDiskPath = QString(); + + // Default to Latin encoding + mIniCodec = QTextCodec::codecForName("windows-1252"); + + mInstallMorrowind = false; + mInstallTribunal = false; + mInstallBloodmoon = false; + + mMorrowindDone = false; + mTribunalDone = false; + mBloodmoonDone = false; + + mStopped = false; + + qRegisterMetaType("Wizard::Component"); +} + +Wizard::UnshieldWorker::~UnshieldWorker() +{ +} + +void Wizard::UnshieldWorker::stopWorker() +{ + mMutex.lock(); + mStopped = true; + mMutex.unlock(); +} + +void Wizard::UnshieldWorker::setInstallComponent(Wizard::Component component, bool install) +{ + QWriteLocker writeLock(&mLock); + switch (component) { + + case Wizard::Component_Morrowind: + mInstallMorrowind = install; + break; + case Wizard::Component_Tribunal: + mInstallTribunal = install; + break; + case Wizard::Component_Bloodmoon: + mInstallBloodmoon = install; + break; + } +} + +bool Wizard::UnshieldWorker::getInstallComponent(Component component) +{ + QReadLocker readLock(&mLock); + switch (component) { + + case Wizard::Component_Morrowind: + return mInstallMorrowind; + case Wizard::Component_Tribunal: + return mInstallTribunal; + case Wizard::Component_Bloodmoon: + return mInstallBloodmoon; + } + + return false; +} + +void Wizard::UnshieldWorker::setComponentDone(Component component, bool done) +{ + QWriteLocker writeLock(&mLock); + switch (component) { + + case Wizard::Component_Morrowind: + mMorrowindDone = done; + break; + case Wizard::Component_Tribunal: + mTribunalDone = done; + break; + case Wizard::Component_Bloodmoon: + mBloodmoonDone = done; + break; + } +} + +bool Wizard::UnshieldWorker::getComponentDone(Component component) +{ + QReadLocker readLock(&mLock); + switch (component) + { + + case Wizard::Component_Morrowind: + return mMorrowindDone; + case Wizard::Component_Tribunal: + return mTribunalDone; + case Wizard::Component_Bloodmoon: + return mBloodmoonDone; + } + + return false; +} + +void Wizard::UnshieldWorker::setPath(const QString &path) +{ + QWriteLocker writeLock(&mLock); + mPath = path; +} + +void Wizard::UnshieldWorker::setIniPath(const QString &path) +{ + QWriteLocker writeLock(&mLock); + mIniPath = path; +} + +void Wizard::UnshieldWorker::setDiskPath(const QString &path) +{ + QWriteLocker writeLock(&mLock); + mDiskPath = path; + mWait.wakeAll(); +} + +QString Wizard::UnshieldWorker::getPath() +{ + QReadLocker readLock(&mLock); + return mPath; +} + +QString Wizard::UnshieldWorker::getIniPath() +{ + QReadLocker readLock(&mLock); + return mIniPath; +} + +QString Wizard::UnshieldWorker::getDiskPath() +{ + QReadLocker readLock(&mLock); + return mDiskPath; +} + + +void Wizard::UnshieldWorker::setIniCodec(QTextCodec *codec) +{ + QWriteLocker writeLock(&mLock); + mIniCodec = codec; +} + +bool Wizard::UnshieldWorker::setupSettings() +{ + // Create Morrowind.ini settings map + if (getIniPath().isEmpty()) + return false; + + QFile file(getIniPath()); + + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + emit error(tr("Failed to open Morrowind configuration file!"), + tr("Opening %1 failed: %2.").arg(getIniPath(), file.errorString())); + return false; + } + + QTextStream stream(&file); + stream.setCodec(mIniCodec); + + mIniSettings.readFile(stream); + + return true; +} + +bool Wizard::UnshieldWorker::writeSettings() +{ + if (getIniPath().isEmpty()) + return false; + + QFile file(getIniPath()); + + if (!file.open(QIODevice::ReadWrite | QIODevice::Text)) { + emit error(tr("Failed to open Morrowind configuration file!"), + tr("Opening %1 failed: %2.").arg(getIniPath(), file.errorString())); + return false; + } + + QTextStream stream(&file); + stream.setCodec(mIniCodec); + + if (!mIniSettings.writeFile(getIniPath(), stream)) { + emit error(tr("Failed to write Morrowind configuration file!"), + tr("Writing to %1 failed: %2.").arg(getIniPath(), file.errorString())); + return false; + } + + return true; +} + +bool Wizard::UnshieldWorker::removeDirectory(const QString &dirName) +{ + bool result = false; + QDir dir(dirName); + + if (dir.exists(dirName)) + { + QFileInfoList list(dir.entryInfoList(QDir::NoDotAndDotDot | + QDir::System | QDir::Hidden | + QDir::AllDirs | QDir::Files, QDir::DirsFirst)); + foreach(QFileInfo info, list) { + if (info.isDir()) { + result = removeDirectory(info.absoluteFilePath()); + } else { + result = QFile::remove(info.absoluteFilePath()); + } + + if (!result) + return result; + } + + result = dir.rmdir(dirName); + } + return result; +} + +bool Wizard::UnshieldWorker::copyFile(const QString &source, const QString &destination, bool keepSource) +{ + QDir dir; + QFile file; + + QFileInfo info(destination); + + if (info.exists()) { + if (!dir.remove(info.absoluteFilePath())) + return false; + } + + if (file.copy(source, destination)) { + if (!keepSource) { + if (!file.remove(source)) + return false; + } else { + return true; + } + } else { + return false; + } + + return true; +} + +bool Wizard::UnshieldWorker::copyDirectory(const QString &source, const QString &destination, bool keepSource) +{ + QDir sourceDir(source); + QDir destDir(destination); + bool result = true; + + if (!destDir.exists()) { + if (!sourceDir.mkpath(destination)) + return false; + } + + destDir.refresh(); + + if (!destDir.exists()) + return false; + + QFileInfoList list(sourceDir.entryInfoList(QDir::NoDotAndDotDot | + QDir::System | QDir::Hidden | + QDir::AllDirs | QDir::Files, QDir::DirsFirst)); + + foreach (const QFileInfo &info, list) { + QString relativePath(info.absoluteFilePath()); + relativePath.remove(source); + + QString destinationPath(destDir.absolutePath() + relativePath); + + if (info.isSymLink()) + continue; + + if (info.isDir()) { + result = copyDirectory(info.absoluteFilePath(), destinationPath); + } else { + result = copyFile(info.absoluteFilePath(), destinationPath); + } + } + + if (!keepSource) + return result && removeDirectory(sourceDir.absolutePath()); + + return result; +} + +bool Wizard::UnshieldWorker::installFile(const QString &fileName, const QString &path, Qt::MatchFlags flags, bool keepSource) +{ + return installFiles(fileName, path, flags, true, keepSource); +} + +bool Wizard::UnshieldWorker::installFiles(const QString &fileName, const QString &path, Qt::MatchFlags flags, bool keepSource, bool single) +{ + QDir dir(path); + + if (!dir.exists()) + return false; + + QStringList files(findFiles(fileName, path, flags)); + + foreach (const QString &file, files) { + QFileInfo info(file); + emit textChanged(tr("Installing: %1").arg(info.fileName())); + + if (single) { + return copyFile(info.absoluteFilePath(), getPath() + QDir::separator() + info.fileName(), keepSource); + } else { + if (!copyFile(info.absoluteFilePath(), getPath() + QDir::separator() + info.fileName(), keepSource)) + return false; + } + } + + return true; +} + +bool Wizard::UnshieldWorker::installDirectories(const QString &dirName, const QString &path, bool recursive, bool keepSource) +{ + QDir dir(path); + + if (!dir.exists()) + return false; + + QStringList directories(findDirectories(dirName, path, recursive)); + + foreach (const QString &dir, directories) { + QFileInfo info(dir); + emit textChanged(tr("Installing: %1 directory").arg(info.fileName())); + if (!copyDirectory(info.absoluteFilePath(), getPath() + QDir::separator() + info.fileName(), keepSource)) + return false; + } + + return true; +} + +void Wizard::UnshieldWorker::extract() +{ + if (getInstallComponent(Wizard::Component_Morrowind)) + { + if (!getComponentDone(Wizard::Component_Morrowind)) + if (!setupComponent(Wizard::Component_Morrowind)) + return; + } + + if (getInstallComponent(Wizard::Component_Tribunal)) + { + if (!getComponentDone(Wizard::Component_Tribunal)) + if (!setupComponent(Wizard::Component_Tribunal)) + return; + } + + if (getInstallComponent(Wizard::Component_Bloodmoon)) + { + if (!getComponentDone(Wizard::Component_Bloodmoon)) + if (!setupComponent(Wizard::Component_Bloodmoon)) + return; + } + + // Update Morrowind configuration + if (getInstallComponent(Wizard::Component_Tribunal)) + { + mIniSettings.setValue(QLatin1String("Archives/Archive 0"), QVariant(QString("Tribunal.bsa"))); + mIniSettings.setValue(QLatin1String("Game Files/GameFile1"), QVariant(QString("Tribunal.esm"))); + } + + if (getInstallComponent(Wizard::Component_Bloodmoon)) + { + mIniSettings.setValue(QLatin1String("Archives/Archive 0"), QVariant(QString("Bloodmoon.bsa"))); + mIniSettings.setValue(QLatin1String("Game Files/GameFile1"), QVariant(QString("Bloodmoon.esm"))); + } + + if (getInstallComponent(Wizard::Component_Tribunal) && + getInstallComponent(Wizard::Component_Bloodmoon)) + { + mIniSettings.setValue(QLatin1String("Archives/Archive 0"), QVariant(QString("Tribunal.bsa"))); + mIniSettings.setValue(QLatin1String("Archives/Archive 1"), QVariant(QString("Bloodmoon.bsa"))); + mIniSettings.setValue(QLatin1String("Game Files/GameFile1"), QVariant(QString("Tribunal.esm"))); + mIniSettings.setValue(QLatin1String("Game Files/GameFile2"), QVariant(QString("Bloodmoon.esm"))); + } + + // Write the settings to the Morrowind config file + if (!writeSettings()) + return; + + // Remove the temporary directory + removeDirectory(getPath() + QDir::separator() + QLatin1String("extract-temp")); + + // Fill the progress bar + int total = 0; + + if (getInstallComponent(Wizard::Component_Morrowind)) + total = 100; + + if (getInstallComponent(Wizard::Component_Tribunal)) + total = total + 100; + + if (getInstallComponent(Wizard::Component_Bloodmoon)) + total = total + 100; + + emit textChanged(tr("Installation finished!")); + emit progressChanged(total); + emit finished(); +} + +bool Wizard::UnshieldWorker::setupComponent(Component component) +{ + QString name; + switch (component) { + + case Wizard::Component_Morrowind: + name = QLatin1String("Morrowind"); + break; + case Wizard::Component_Tribunal: + name = QLatin1String("Tribunal"); + break; + case Wizard::Component_Bloodmoon: + name = QLatin1String("Bloodmoon"); + break; + } + + if (name.isEmpty()) { + emit error(tr("Component parameter is invalid!"), tr("An invalid component parameter was supplied.")); + return false; + } + + bool found = false; + QString cabFile; + QDir disk; + + // Keep showing the file dialog until we find the necessary install files + while (!found) { + if (getDiskPath().isEmpty()) { + QReadLocker readLock(&mLock); + emit requestFileDialog(component); + mWait.wait(&mLock); + disk.setPath(getDiskPath()); + } else { + disk.setPath(getDiskPath()); + } + + QStringList list(findFiles(QLatin1String("data1.hdr"), disk.absolutePath())); + + foreach (const QString &file, list) { + + qDebug() << "current archive: " << file; + + if (component == Wizard::Component_Morrowind) + { + bool morrowindFound = findInCab(QLatin1String("Morrowind.bsa"), file); + bool tribunalFound = findInCab(QLatin1String("Tribunal.bsa"), file); + bool bloodmoonFound = findInCab(QLatin1String("Bloodmoon.bsa"), file); + + if (morrowindFound) { + // Check if we have correct archive, other archives have Morrowind.bsa too + if ((tribunalFound && bloodmoonFound) + || (!tribunalFound && !bloodmoonFound)) { + cabFile = file; + found = true; // We have a GoTY disk or a Morrowind-only disk + } + } + } else { + + if (findInCab(name + QLatin1String(".bsa"), file)) { + cabFile = file; + found = true; + } + } + + } + + if (!found) { + QReadLocker readLock(&mLock); + emit requestFileDialog(component); + mWait.wait(&mLock); + } + } + + if (installComponent(component, cabFile)) { + setComponentDone(component, true); + return true; + } else { + return false; + } + + return true; +} + +bool Wizard::UnshieldWorker::installComponent(Component component, const QString &path) +{ + QString name; + switch (component) { + + case Wizard::Component_Morrowind: + name = QLatin1String("Morrowind"); + break; + case Wizard::Component_Tribunal: + name = QLatin1String("Tribunal"); + break; + case Wizard::Component_Bloodmoon: + name = QLatin1String("Bloodmoon"); + break; + } + + if (name.isEmpty()) { + emit error(tr("Component parameter is invalid!"), tr("An invalid component parameter was supplied.")); + return false; + } + + emit textChanged(tr("Installing %1").arg(name)); + + QFileInfo info(path); + + if (!info.exists()) { + emit error(tr("Installation media path not set!"), tr("The source path for %1 was not set.").arg(name)); + return false; + } + + // Create temporary extract directory + // TODO: Use QTemporaryDir in Qt 5.0 + QString tempPath(getPath() + QDir::separator() + QLatin1String("extract-temp")); + QDir temp; + + // Make sure the temporary folder is empty + removeDirectory(tempPath); + + if (!temp.mkpath(tempPath)) { + emit error(tr("Cannot create temporary directory!"), tr("Failed to create %1.").arg(tempPath)); + return false; + } + + temp.setPath(tempPath); + + if (!temp.mkdir(name)) { + emit error(tr("Cannot create temporary directory!"), tr("Failed to create %1.").arg(temp.absoluteFilePath(name))); + return false; + } + + if (!temp.cd(name)) { + emit error(tr("Cannot move into temporary directory!"), tr("Failed to move into %1.").arg(temp.absoluteFilePath(name))); + return false; + } + + // Extract the installation files + if (!extractCab(info.absoluteFilePath(), temp.absolutePath())) + return false; + + // Move the files from the temporary path to the destination folder + emit textChanged(tr("Moving installation files")); + + // Install extracted directories + QStringList directories; + directories << QLatin1String("BookArt") + << QLatin1String("Fonts") + << QLatin1String("Icons") + << QLatin1String("Meshes") + << QLatin1String("Music") + << QLatin1String("Sound") + << QLatin1String("Splash") + << QLatin1String("Textures") + << QLatin1String("Video"); + + foreach (const QString &dir, directories) { + if (!installDirectories(dir, temp.absolutePath())) { + emit error(tr("Could not install directory!"), + tr("Installing %1 to %2 failed.").arg(dir, temp.absolutePath())); + return false; + } + } + + // Install directories from disk + foreach (const QString &dir, directories) { + if (!installDirectories(dir, info.absolutePath(), false, true)) { + emit error(tr("Could not install directory!"), + tr("Installing %1 to %2 failed.").arg(dir, info.absolutePath())); + return false; + } + + } + + // Install translation files + QStringList extensions; + extensions << QLatin1String(".cel") + << QLatin1String(".top") + << QLatin1String(".mrk"); + + foreach (const QString &extension, extensions) { + if (!installFiles(extension, info.absolutePath(), Qt::MatchEndsWith)) { + emit error(tr("Could not install translation file!"), + tr("Failed to install *%1 files.").arg(extension)); + return false; + } + } + + if (component == Wizard::Component_Morrowind) + { + QStringList files; + files << QLatin1String("Morrowind.esm") + << QLatin1String("Morrowind.bsa"); + + foreach (const QString &file, files) { + if (!installFile(file, temp.absolutePath())) { + emit error(tr("Could not install Morrowind data file!"), + tr("Failed to install %1.").arg(file)); + return false; + } + } + + // Copy Morrowind configuration file + if (!installFile(QLatin1String("Morrowind.ini"), temp.absolutePath())) { + emit error(tr("Could not install Morrowind configuration file!"), + tr("Failed to install %1.").arg(QLatin1String("Morrowind.ini"))); + return false; + } + + // Setup Morrowind configuration + setIniPath(getPath() + QDir::separator() + QLatin1String("Morrowind.ini")); + + if (!setupSettings()) + return false; + } + + if (component == Wizard::Component_Tribunal) + { + QFileInfo sounds(temp.absoluteFilePath(QLatin1String("Sounds"))); + QString dest(getPath() + QDir::separator() + QLatin1String("Sound")); + + if (sounds.exists()) { + emit textChanged(tr("Installing: Sound directory")); + if (!copyDirectory(sounds.absoluteFilePath(), dest)) { + emit error(tr("Could not install directory!"), + tr("Installing %1 to %2 failed.").arg(sounds.absoluteFilePath(), dest)); + return false; + } + + } + + QStringList files; + files << QLatin1String("Tribunal.esm") + << QLatin1String("Tribunal.bsa"); + + foreach (const QString &file, files) { + if (!installFile(file, temp.absolutePath())) { + emit error(tr("Could not find Tribunal data file!"), + tr("Failed to find %1.").arg(file)); + return false; + } + } + } + + if (component == Wizard::Component_Bloodmoon) + { + QFileInfo original(getPath() + QDir::separator() + QLatin1String("Tribunal.esm")); + + if (original.exists()) { + if (!installFile(QLatin1String("Tribunal.esm"), temp.absolutePath())) { + emit error(tr("Could not find Tribunal patch file!"), + tr("Failed to find %1.").arg(QLatin1String("Tribunal.esm"))); + return false; + } + } + + QStringList files; + files << QLatin1String("Bloodmoon.esm") + << QLatin1String("Bloodmoon.bsa"); + + foreach (const QString &file, files) { + if (!installFile(file, temp.absolutePath())) { + emit error(tr("Could not find Bloodmoon data file!"), + tr("Failed to find %1.").arg(file)); + return false; + } + } + + // Load Morrowind configuration settings from the setup script + QStringList list(findFiles(QLatin1String("setup.inx"), getDiskPath())); + + emit textChanged(tr("Updating Morrowind configuration file")); + + foreach (const QString &inx, list) { + mIniSettings.parseInx(inx); + } + } + + // Finally, install Data Files directories from temp and disk + QStringList datafiles(findDirectories(QLatin1String("Data Files"), temp.absolutePath())); + datafiles.append(findDirectories(QLatin1String("Data Files"), info.absolutePath())); + + foreach (const QString &dir, datafiles) { + QFileInfo info(dir); + emit textChanged(tr("Installing: %1 directory").arg(info.fileName())); + + if (!copyDirectory(info.absoluteFilePath(), getPath())) { + emit error(tr("Could not install directory!"), + tr("Installing %1 to %2 failed.").arg(info.absoluteFilePath(), getPath())); + return false; + } + } + + emit textChanged(tr("%1 installation finished!").arg(name)); + return true; + +} + +bool Wizard::UnshieldWorker::extractFile(Unshield *unshield, const QString &destination, const QString &prefix, int index, int counter) +{ + bool success = false; + QString path(destination); + path.append(QDir::separator()); + + int directory = unshield_file_directory(unshield, index); + + if (!prefix.isEmpty()) + path.append(prefix + QDir::separator()); + + if (directory >= 0) + path.append(QString::fromUtf8(unshield_directory_name(unshield, directory)) + QDir::separator()); + + // Ensure the path has the right separators + path.replace(QLatin1Char('\\'), QDir::separator()); + path = QDir::toNativeSeparators(path); + + // Ensure the target path exists + QDir dir; + if (!dir.mkpath(path)) + return false; + + QString fileName(path); + fileName.append(QString::fromUtf8(unshield_file_name(unshield, index))); + + // Calculate the percentage done + int progress = (((float) counter / (float) unshield_file_count(unshield)) * 100); + + if (getComponentDone(Wizard::Component_Morrowind)) + progress = progress + 100; + + if (getComponentDone(Wizard::Component_Tribunal)) + progress = progress + 100; + + emit textChanged(tr("Extracting: %1").arg(QString::fromUtf8(unshield_file_name(unshield, index)))); + emit progressChanged(progress); + + QByteArray array(fileName.toUtf8()); + success = unshield_file_save(unshield, index, array.constData()); + + if (!success) { + qDebug() << "error"; + dir.remove(fileName); + } + + return success; +} + +bool Wizard::UnshieldWorker::extractCab(const QString &cabFile, const QString &destination) +{ + bool success = false; + + QByteArray array(cabFile.toUtf8()); + + Unshield *unshield; + unshield = unshield_open(array.constData()); + + if (!unshield) { + emit error(tr("Failed to open InstallShield Cabinet File."), tr("Opening %1 failed.").arg(cabFile)); + unshield_close(unshield); + return false; + } + + int counter = 0; + + for (int i=0; ifirst_file; j<=group->last_file; ++j) + { + if (mStopped) { + qDebug() << "We're asked to stop!"; + + unshield_close(unshield); + return true; + } + + if (unshield_file_is_valid(unshield, j)) { + success = extractFile(unshield, destination, group->name, j, counter); + + if (!success) { + QString name(QString::fromUtf8(unshield_file_name(unshield, j))); + + emit error(tr("Failed to extract %1.").arg(name), + tr("Complete path: %1").arg(destination + QDir::separator() + name)); + + unshield_close(unshield); + return false; + } + + ++counter; + } + } + } + + unshield_close(unshield); + return success; +} + +QString Wizard::UnshieldWorker::findFile(const QString &fileName, const QString &path) +{ + return findFiles(fileName, path).first(); +} + +QStringList Wizard::UnshieldWorker::findFiles(const QString &fileName, const QString &path, int depth, bool recursive, + bool directories, Qt::MatchFlags flags) +{ + static const int MAXIMUM_DEPTH = 10; + + if (depth >= MAXIMUM_DEPTH) { + qWarning("Maximum directory depth limit reached."); + return QStringList(); + } + + QStringList result; + QDir dir(path); + + // Prevent parsing over the complete filesystem + if (dir == QDir::rootPath()) + return QStringList(); + + if (!dir.exists()) + return QStringList(); + + QFileInfoList list(dir.entryInfoList(QDir::NoDotAndDotDot | + QDir::AllDirs | QDir::Files, QDir::DirsFirst)); + foreach(QFileInfo info, list) { + if (info.isSymLink()) + continue; + + if (info.isDir()) { + if (directories) + { + if (info.fileName() == fileName) { + result.append(info.absoluteFilePath()); + } else { + if (recursive) + result.append(findFiles(fileName, info.absoluteFilePath(), depth + 1, recursive, true)); + } + } else { + if (recursive) + result.append(findFiles(fileName, info.absoluteFilePath(), depth + 1)); + } + } else { + if (directories) + break; + + switch (flags) { + case Qt::MatchExactly: + if (info.fileName() == fileName) + result.append(info.absoluteFilePath()); + break; + case Qt::MatchEndsWith: + if (info.fileName().endsWith(fileName)) + result.append(info.absoluteFilePath()); + break; + } + } + } + + return result; +} + +QStringList Wizard::UnshieldWorker::findDirectories(const QString &dirName, const QString &path, bool recursive) +{ + return findFiles(dirName, path, 0, true, true); +} + +bool Wizard::UnshieldWorker::findInCab(const QString &fileName, const QString &cabFile) +{ + QByteArray array(cabFile.toUtf8()); + + Unshield *unshield; + unshield = unshield_open(array.constData()); + + if (!unshield) { + emit error(tr("Failed to open InstallShield Cabinet File."), tr("Opening %1 failed.").arg(cabFile)); + unshield_close(unshield); + return false; + } + + for (int i=0; ifirst_file; j<=group->last_file; ++j) + { + + if (unshield_file_is_valid(unshield, j)) { + QString current(QString::fromUtf8(unshield_file_name(unshield, j))); + if (current.toLower() == fileName.toLower()) { + unshield_close(unshield); + return true; // File is found! + } + } + } + } + + unshield_close(unshield); + return false; +} diff --git a/apps/wizard/unshield/unshieldworker.hpp b/apps/wizard/unshield/unshieldworker.hpp new file mode 100644 index 000000000..5ea7b04ae --- /dev/null +++ b/apps/wizard/unshield/unshieldworker.hpp @@ -0,0 +1,127 @@ +#ifndef UNSHIELDWORKER_HPP +#define UNSHIELDWORKER_HPP + +#include +#include +#include +#include +#include +#include + +#include + +#include "../inisettings.hpp" + + +namespace Wizard +{ + enum Component { + Component_Morrowind, + Component_Tribunal, + Component_Bloodmoon + }; + + class UnshieldWorker : public QObject + { + Q_OBJECT + Q_ENUMS(Wizard::Component) + + public: + UnshieldWorker(QObject *parent = 0); + ~UnshieldWorker(); + + void stopWorker(); + + void setInstallComponent(Wizard::Component component, bool install); + + void setDiskPath(const QString &path); + + void setPath(const QString &path); + void setIniPath(const QString &path); + + QString getPath(); + QString getIniPath(); + + void setIniCodec(QTextCodec *codec); + + bool setupSettings(); + + private: + + bool writeSettings(); + + bool getInstallComponent(Component component); + + QString getDiskPath(); + + void setComponentDone(Component component, bool done = true); + bool getComponentDone(Component component); + + bool removeDirectory(const QString &dirName); + + bool copyFile(const QString &source, const QString &destination, bool keepSource = true); + bool copyDirectory(const QString &source, const QString &destination, bool keepSource = true); + + bool extractCab(const QString &cabFile, const QString &destination); + bool extractFile(Unshield *unshield, const QString &destination, const QString &prefix, int index, int counter); + + bool findInCab(const QString &fileName, const QString &cabFile); + + QString findFile(const QString &fileName, const QString &path); + + QStringList findFiles(const QString &fileName, const QString &path, int depth = 0, bool recursive = true, + bool directories = false, Qt::MatchFlags flags = Qt::MatchExactly); + + QStringList findDirectories(const QString &dirName, const QString &path, bool recursive = true); + + bool installFile(const QString &fileName, const QString &path, Qt::MatchFlags flags = Qt::MatchExactly, + bool keepSource = false); + + bool installFiles(const QString &fileName, const QString &path, Qt::MatchFlags flags = Qt::MatchExactly, + bool keepSource = false, bool single = false); + + bool installDirectories(const QString &dirName, const QString &path, + bool recursive = true, bool keepSource = false); + + bool installComponent(Component component, const QString &path); + bool setupComponent(Component component); + + bool mInstallMorrowind; + bool mInstallTribunal; + bool mInstallBloodmoon; + + bool mMorrowindDone; + bool mTribunalDone; + bool mBloodmoonDone; + + bool mStopped; + + QString mPath; + QString mIniPath; + QString mDiskPath; + + IniSettings mIniSettings; + + QTextCodec *mIniCodec; + + QWaitCondition mWait; + QMutex mMutex; + + QReadWriteLock mLock; + + public slots: + void extract(); + + signals: + void finished(); + void requestFileDialog(Wizard::Component component); + + void textChanged(const QString &text); + + void error(const QString &text, const QString &details); + void progressChanged(int progress); + + }; +} + +#endif // UNSHIELDWORKER_HPP diff --git a/apps/wizard/utils/componentlistwidget.cpp b/apps/wizard/utils/componentlistwidget.cpp new file mode 100644 index 000000000..6a5d019b5 --- /dev/null +++ b/apps/wizard/utils/componentlistwidget.cpp @@ -0,0 +1,48 @@ +#include "componentlistwidget.hpp" + +#include +#include + +ComponentListWidget::ComponentListWidget(QWidget *parent) : + QListWidget(parent) +{ + mCheckedItems = QStringList(); + + connect(this, SIGNAL(itemChanged(QListWidgetItem *)), + this, SLOT(updateCheckedItems(QListWidgetItem *))); + + connect(model(), SIGNAL(rowsInserted(QModelIndex, int, int)), + this, SLOT(updateCheckedItems(QModelIndex, int, int))); +} + +QStringList ComponentListWidget::checkedItems() +{ + mCheckedItems.removeDuplicates(); + return mCheckedItems; +} + +void ComponentListWidget::updateCheckedItems(const QModelIndex &index, int start, int end) +{ + updateCheckedItems(item(start)); +} + +void ComponentListWidget::updateCheckedItems(QListWidgetItem *item) +{ + if (!item) + return; + + QString text = item->text(); + + if (item->checkState() == Qt::Checked) { + if (!mCheckedItems.contains(text)) + mCheckedItems.append(text); + } else { + if (mCheckedItems.contains(text)) + mCheckedItems.removeAll(text); + } + + mCheckedItems.removeDuplicates(); + + emit checkedItemsChanged(mCheckedItems); + +} diff --git a/apps/wizard/utils/componentlistwidget.hpp b/apps/wizard/utils/componentlistwidget.hpp new file mode 100644 index 000000000..23965f8a6 --- /dev/null +++ b/apps/wizard/utils/componentlistwidget.hpp @@ -0,0 +1,26 @@ +#ifndef COMPONENTLISTWIDGET_HPP +#define COMPONENTLISTWIDGET_HPP + +#include + +class ComponentListWidget : public QListWidget +{ + Q_OBJECT + + Q_PROPERTY(QStringList mCheckedItems READ checkedItems) + +public: + ComponentListWidget(QWidget *parent = 0); + + QStringList mCheckedItems; + QStringList checkedItems(); + +signals: + void checkedItemsChanged(const QStringList &items); + +private slots: + void updateCheckedItems(QListWidgetItem *item); + void updateCheckedItems(const QModelIndex &index, int start, int end); +}; + +#endif // COMPONENTLISTWIDGET_HPP diff --git a/cmake/FindFFmpeg.cmake b/cmake/FindFFmpeg.cmake index a3509597b..74584bf31 100644 --- a/cmake/FindFFmpeg.cmake +++ b/cmake/FindFFmpeg.cmake @@ -14,6 +14,7 @@ # - AVUTIL # - POSTPROCESS # - SWSCALE +# - SWRESAMPLE # the following variables will be defined # _FOUND - System has # _INCLUDE_DIRS - Include directory necessary for using the headers @@ -112,6 +113,8 @@ if (NOT FFMPEG_LIBRARIES) find_component(AVUTIL libavutil avutil libavutil/avutil.h) find_component(SWSCALE libswscale swscale libswscale/swscale.h) find_component(POSTPROC libpostproc postproc libpostproc/postprocess.h) + find_component(SWRESAMPLE libswresample swresample libswresample/swresample.h) + find_component(AVRESAMPLE libavresample avresample libavresample/avresample.h) # Check if the required components were found and add their stuff to the FFMPEG_* vars. foreach (_component ${FFmpeg_FIND_COMPONENTS}) @@ -142,7 +145,7 @@ if (NOT FFMPEG_LIBRARIES) endif () # Now set the noncached _FOUND vars for the components. -foreach (_component AVCODEC AVDEVICE AVFORMAT AVUTIL POSTPROCESS SWSCALE) +foreach (_component AVCODEC AVDEVICE AVFORMAT AVUTIL POSTPROCESS SWSCALE SWRESAMPLE AVRESAMPLE) set_component_found(${_component}) endforeach () diff --git a/cmake/FindGMock.cmake b/cmake/FindGMock.cmake deleted file mode 100644 index eda7d4d72..000000000 --- a/cmake/FindGMock.cmake +++ /dev/null @@ -1,91 +0,0 @@ -# Locate the Google C++ Mocking Framework. -# -# Defines the following variables: -# -# GMOCK_FOUND - Found the Google Mocking framework -# GMOCK_INCLUDE_DIRS - Include directories -# -# Also defines the library variables below as normal -# variables. These contain debug/optimized keywords when -# a debugging library is found. -# -# GMOCK_BOTH_LIBRARIES - Both libgmock & libgmock-main -# GMOCK_LIBRARIES - libgmock -# GMOCK_MAIN_LIBRARIES - libgmock-main -# -# Accepts the following variables as input: -# -# GMOCK_ROOT - (as CMake or env. variable) -# The root directory of the gmock install prefix -# -#----------------------- -# Example Usage: -# -# enable_testing(true) -# find_package(GMock REQUIRED) -# include_directories(${GMOCK_INCLUDE_DIRS}) -# -# add_executable(foo foo.cc) -# target_link_libraries(foo ${GMOCK_BOTH_LIBRARIES}) -# -# add_test(AllTestsInFoo foo) -# - -#set (GMOCK_FOUND FALSE) - - -#set (GMOCK_ROOT $ENV{GMOCK_ROOT} CACHE PATH "Path to the gmock root directory.") -if (NOT EXISTS ${GMOCK_ROOT}) - message (FATAL_ERROR "GMOCK_ROOT does not exist.") -endif () - -#set (GMOCK_BUILD ${GMOCK_ROOT}/build CACHE PATH "Path to the gmock build directory.") -if (NOT EXISTS ${GMOCK_BUILD}) - message (FATAL_ERROR "GMOCK_BUILD does not exist.") -endif () - -# Find the include directory -find_path(GMOCK_INCLUDE_DIRS gmock/gmock.h - HINTS - $ENV{GMOCK_ROOT}/include - ${GMOCK_ROOT}/include -) -mark_as_advanced(GMOCK_INCLUDE_DIRS) - -function(_gmock_find_library _name) - find_library(${_name} - NAMES ${ARGN} - HINTS - $ENV{GMOCK_BUILD} - ${GMOCK_BUILD} - ) - mark_as_advanced(${_name}) -endfunction() - -# Find the gmock libraries -if (MSVC) - _gmock_find_library (GMOCK_LIBRARIES_DEBUG gmock ${GMOCK_BUILD}/Debug) - _gmock_find_library (GMOCK_LIBRARIES_RELEASE gmock ${GMOCK_BUILD}/Release) - _gmock_find_library (GMOCK_MAIN_LIBRARIES_DEBUG gmock_main ${GMOCK_BUILD}/Debug) - _gmock_find_library (GMOCK_MAIN_LIBRARIES_RELEASE gmock_main ${GMOCK_BUILD}/Release) - set (GMOCK_LIBRARIES - debug ${GMOCK_LIBRARIES_DEBUG} - optimized ${GMOCK_LIBRARIES_RELEASE} - ) - set (GMOCK_MAIN_LIBRARIES - debug ${GMOCK_MAIN_LIBRARIES_DEBUG} - optimized ${GMOCK_MAIN_LIBRARIES_RELEASE} - ) -else () - _gmock_find_library (GMOCK_LIBRARIES gmock ${GMOCK_BUILD}) - _gmock_find_library (GMOCK_MAIN_LIBRARIES gmock_main ${GMOCK_BUILD} ${GMOCK_BUILD}/Debug) -endif () - -FIND_PACKAGE_HANDLE_STANDARD_ARGS(GMock DEFAULT_MSG GMOCK_LIBRARIES GMOCK_INCLUDE_DIRS GMOCK_MAIN_LIBRARIES) - -if(GMOCK_FOUND) - set(GMOCK_INCLUDE_DIRS ${GMOCK_INCLUDE_DIR}) - set(GMOCK_BOTH_LIBRARIES ${GMOCK_LIBRARIES} ${GMOCK_MAIN_LIBRARIES}) -endif() - - diff --git a/cmake/FindMyGUI.cmake b/cmake/FindMyGUI.cmake index ebf069404..40fa2373f 100644 --- a/cmake/FindMyGUI.cmake +++ b/cmake/FindMyGUI.cmake @@ -12,6 +12,7 @@ # For details see the accompanying COPYING-CMAKE-SCRIPTS file. CMAKE_POLICY(PUSH) include(FindPkgMacros) +include(PreprocessorUtils) # IF (MYGUI_LIBRARIES AND MYGUI_INCLUDE_DIRS) # SET(MYGUI_FIND_QUIETLY TRUE) @@ -131,6 +132,18 @@ IF (MYGUI_FOUND) MESSAGE(STATUS " libraries : ${MYGUI_LIBRARIES} from ${MYGUI_LIB_DIR}") MESSAGE(STATUS " includes : ${MYGUI_INCLUDE_DIRS}") ENDIF (NOT MYGUI_FIND_QUIETLY) + + find_file(MYGUI_PREQUEST_FILE NAMES MyGUI_Prerequest.h PATHS ${MYGUI_INCLUDE_DIRS}) + file(READ ${MYGUI_PREQUEST_FILE} MYGUI_TEMP_VERSION_CONTENT) + get_preprocessor_entry(MYGUI_TEMP_VERSION_CONTENT MYGUI_VERSION_MAJOR MYGUI_VERSION_MAJOR) + get_preprocessor_entry(MYGUI_TEMP_VERSION_CONTENT MYGUI_VERSION_MINOR MYGUI_VERSION_MINOR) + get_preprocessor_entry(MYGUI_TEMP_VERSION_CONTENT MYGUI_VERSION_PATCH MYGUI_VERSION_PATCH) + set(MYGUI_VERSION "${MYGUI_VERSION_MAJOR}.${MYGUI_VERSION_MINOR}.${MYGUI_VERSION_PATCH}") + + IF (NOT MYGUI_FIND_QUIETLY) + MESSAGE(STATUS "MyGUI version: ${MYGUI_VERSION}") + ENDIF (NOT MYGUI_FIND_QUIETLY) + ELSE (MYGUI_FOUND) IF (MYGUI_FIND_REQUIRED) MESSAGE(FATAL_ERROR "Could not find MYGUI") diff --git a/cmake/FindOGRE.cmake b/cmake/FindOGRE.cmake index 81b52b1b7..f2acf9d33 100644 --- a/cmake/FindOGRE.cmake +++ b/cmake/FindOGRE.cmake @@ -18,7 +18,7 @@ # Once done, this will define # # OGRE_FOUND - system has OGRE -# OGRE_INCLUDE_DIRS - the OGRE include directories +# OGRE_INCLUDE_DIRS - the OGRE include directories # OGRE_LIBRARIES - link these to use the OGRE core # OGRE_BINARY_REL - location of the main Ogre binary (win32 non-static only, release) # OGRE_BINARY_DBG - location of the main Ogre binaries (win32 non-static only, debug) @@ -35,7 +35,7 @@ # # OGRE_${COMPONENT}_FOUND - ${COMPONENT} is available # OGRE_${COMPONENT}_INCLUDE_DIRS - additional include directories for ${COMPONENT} -# OGRE_${COMPONENT}_LIBRARIES - link these to use ${COMPONENT} +# OGRE_${COMPONENT}_LIBRARIES - link these to use ${COMPONENT} # OGRE_${COMPONENT}_BINARY_REL - location of the component binary (win32 non-static only, release) # OGRE_${COMPONENT}_BINARY_DBG - location of the component binary (win32 non-static only, debug) # @@ -110,9 +110,9 @@ if (OGRE_PREFIX_SOURCE AND OGRE_PREFIX_BUILD) set(OGRE_INC_SEARCH_PATH ${dir}/include ${OGRE_INC_SEARCH_PATH}) set(OGRE_LIB_SEARCH_PATH ${dir}/lib ${OGRE_LIB_SEARCH_PATH}) set(OGRE_BIN_SEARCH_PATH ${dir}/bin ${OGRE_BIN_SEARCH_PATH}) - set(OGRE_BIN_SEARCH_PATH ${dir}/Samples/Common/bin ${OGRE_BIN_SEARCH_PATH}) + set(OGRE_BIN_SEARCH_PATH ${dir}/Samples/Common/bin ${OGRE_BIN_SEARCH_PATH}) endforeach(dir) - + if (OGRE_PREFIX_DEPENDENCIES_DIR) set(OGRE_INC_SEARCH_PATH ${OGRE_PREFIX_DEPENDENCIES_DIR}/include ${OGRE_INC_SEARCH_PATH}) set(OGRE_LIB_SEARCH_PATH ${OGRE_PREFIX_DEPENDENCIES_DIR}/lib ${OGRE_LIB_SEARCH_PATH}) @@ -124,12 +124,12 @@ else() endif () # redo search if any of the environmental hints changed -set(OGRE_COMPONENTS Paging Terrain +set(OGRE_COMPONENTS Paging Terrain Overlay Plugin_BSPSceneManager Plugin_CgProgramManager Plugin_OctreeSceneManager Plugin_OctreeZone Plugin_PCZSceneManager Plugin_ParticleFX - RenderSystem_Direct3D10 RenderSystem_Direct3D9 RenderSystem_GL RenderSystem_GLES) -set(OGRE_RESET_VARS - OGRE_CONFIG_INCLUDE_DIR OGRE_INCLUDE_DIR + RenderSystem_Direct3D10 RenderSystem_Direct3D9 RenderSystem_GL RenderSystem_GLES2) +set(OGRE_RESET_VARS + OGRE_CONFIG_INCLUDE_DIR OGRE_INCLUDE_DIR OGRE_LIBRARY_FWK OGRE_LIBRARY_REL OGRE_LIBRARY_DBG OGRE_PLUGIN_DIR_DBG OGRE_PLUGIN_DIR_REL OGRE_MEDIA_DIR) foreach (comp ${OGRE_COMPONENTS}) @@ -148,7 +148,7 @@ if(NOT OGRE_BUILD_PLATFORM_IPHONE AND APPLE) # try to find framework on OSX findpkg_framework(OGRE) else() - set(OGRE_LIBRARY_FWK "") + set(OGRE_LIBRARY_FWK "") endif() # locate Ogre include files @@ -215,8 +215,8 @@ list(REMOVE_DUPLICATES OGRE_INCLUDE_DIR) findpkg_finish(OGRE) add_parent_dir(OGRE_INCLUDE_DIRS OGRE_INCLUDE_DIR) if (OGRE_SOURCE) - # If working from source rather than SDK, add samples include - set(OGRE_INCLUDE_DIRS ${OGRE_INCLUDE_DIRS} "${OGRE_SOURCE}/Samples/Common/include") + # If working from source rather than SDK, add samples include + set(OGRE_INCLUDE_DIRS ${OGRE_INCLUDE_DIRS} "${OGRE_SOURCE}/Samples/Common/include") endif() mark_as_advanced(OGRE_CONFIG_INCLUDE_DIR OGRE_MEDIA_DIR OGRE_PLUGIN_DIR_REL OGRE_PLUGIN_DIR_DBG) @@ -234,10 +234,10 @@ if (OGRE_STATIC) find_package(FreeImage QUIET) find_package(Freetype QUIET) find_package(OpenGL QUIET) - find_package(OpenGLES QUIET) + find_package(OpenGLES2 QUIET) find_package(ZLIB QUIET) find_package(ZZip QUIET) - if (UNIX AND NOT APPLE) + if (UNIX AND (NOT APPLE AND NOT ANDROID)) find_package(X11 QUIET) find_library(XAW_LIBRARY NAMES Xaw Xaw7 PATHS ${DEP_LIB_SEARCH_DIR} ${X11_LIB_SEARCH_PATH}) if (NOT XAW_LIBRARY OR NOT X11_Xt_FOUND) @@ -258,10 +258,16 @@ if (OGRE_STATIC) endif () endif () - set(OGRE_LIBRARIES ${OGRE_LIBRARIES} ${OGRE_LIBRARY_FWK} ${ZZip_LIBRARIES} ${ZLIB_LIBRARIES} - ${FreeImage_LIBRARIES} ${FREETYPE_LIBRARIES} +if (ANDROID) + set(OGRE_LIBRARIES ${OGRE_LIBRARIES} ${OGRE_LIBRARY_FWK} ${ZZip_LIBRARIES} ${ZLIB_LIBRARIES} + ${FreeImage_LIBRARIES} ${FREETYPE_LIBRARIES} + ${Cocoa_LIBRARIES} ${Carbon_LIBRARIES}) +else () + set(OGRE_LIBRARIES ${OGRE_LIBRARIES} ${OGRE_LIBRARY_FWK} ${ZZip_LIBRARIES} ${ZLIB_LIBRARIES} + ${FreeImage_LIBRARIES} ${FREETYPE_LIBRARIES} ${X11_LIBRARIES} ${X11_Xt_LIBRARIES} ${XAW_LIBRARY} ${X11_Xrandr_LIB} ${Cocoa_LIBRARIES} ${Carbon_LIBRARIES}) +endif() if (NOT ZLIB_FOUND OR NOT ZZip_FOUND) set(OGRE_DEPS_FOUND FALSE) @@ -272,10 +278,10 @@ if (OGRE_STATIC) if (NOT FREETYPE_FOUND) set(OGRE_DEPS_FOUND FALSE) endif () - if (UNIX AND NOT APPLE) - if (NOT X11_FOUND) + if (UNIX AND NOT APPLE AND NOT ANDROID) + if (NOT X11_FOUND) set(OGRE_DEPS_FOUND FALSE) - endif () + endif () endif () if (OGRE_CONFIG_THREADS) @@ -305,7 +311,7 @@ if (OGRE_STATIC) endif () endif () endif () - + if (NOT OGRE_DEPS_FOUND) pkg_message(OGRE "Could not find all required dependencies for the Ogre package.") set(OGRE_FOUND FALSE) @@ -323,13 +329,13 @@ set(OGRE_LIBRARY_DIRS ${OGRE_LIBRARY_DIR_REL} ${OGRE_LIBRARY_DIR_DBG}) # find binaries if (NOT OGRE_STATIC) - if (WIN32) - find_file(OGRE_BINARY_REL NAMES "OgreMain.dll" HINTS ${OGRE_BIN_SEARCH_PATH} + if (WIN32) + find_file(OGRE_BINARY_REL NAMES "OgreMain.dll" HINTS ${OGRE_BIN_SEARCH_PATH} PATH_SUFFIXES "" release relwithdebinfo minsizerel) - find_file(OGRE_BINARY_DBG NAMES "OgreMain_d.dll" HINTS ${OGRE_BIN_SEARCH_PATH} + find_file(OGRE_BINARY_DBG NAMES "OgreMain_d.dll" HINTS ${OGRE_BIN_SEARCH_PATH} PATH_SUFFIXES "" debug ) - endif() - mark_as_advanced(OGRE_BINARY_REL OGRE_BINARY_DBG) + endif() + mark_as_advanced(OGRE_BINARY_REL OGRE_BINARY_DBG) endif() @@ -337,7 +343,7 @@ endif() # Find Ogre components ######################################################### -set(OGRE_COMPONENT_SEARCH_PATH_REL +set(OGRE_COMPONENT_SEARCH_PATH_REL ${OGRE_LIBRARY_DIR_REL}/.. ${OGRE_LIBRARY_DIR_REL}/../.. ${OGRE_BIN_SEARCH_PATH} @@ -350,7 +356,7 @@ set(OGRE_COMPONENT_SEARCH_PATH_DBG macro(ogre_find_component COMPONENT HEADER) findpkg_begin(OGRE_${COMPONENT}) - find_path(OGRE_${COMPONENT}_INCLUDE_DIR NAMES ${HEADER} HINTS ${OGRE_INCLUDE_DIRS} ${OGRE_PREFIX_SOURCE} PATH_SUFFIXES ${COMPONENT} OGRE/${COMPONENT} Components/${COMPONENT}/include) + find_path(OGRE_${COMPONENT}_INCLUDE_DIR NAMES ${HEADER} HINTS ${OGRE_INCLUDE_DIRS} ${OGRE_INCLUDE_DIR}/OGRE/${COMPONENT} ${OGRE_PREFIX_SOURCE} PATH_SUFFIXES ${COMPONENT} OGRE/${COMPONENT} Components/${COMPONENT}/include) set(OGRE_${COMPONENT}_LIBRARY_NAMES "Ogre${COMPONENT}${OGRE_LIB_SUFFIX}") get_debug_names(OGRE_${COMPONENT}_LIBRARY_NAMES) find_library(OGRE_${COMPONENT}_LIBRARY_REL NAMES ${OGRE_${COMPONENT}_LIBRARY_NAMES} HINTS ${OGRE_LIBRARY_DIR_REL} PATH_SUFFIXES "" "release" "relwithdebinfo" "minsizerel") @@ -358,19 +364,24 @@ macro(ogre_find_component COMPONENT HEADER) make_library_set(OGRE_${COMPONENT}_LIBRARY) findpkg_finish(OGRE_${COMPONENT}) if (OGRE_${COMPONENT}_FOUND) + if (APPLE) + include_directories("${OGRE_INCLUDE_DIR}/OGRE/${COMPONENT}") + endif() # find binaries if (NOT OGRE_STATIC) - if (WIN32) - find_file(OGRE_${COMPONENT}_BINARY_REL NAMES "Ogre${COMPONENT}.dll" HINTS ${OGRE_COMPONENT_SEARCH_PATH_REL} PATH_SUFFIXES "" bin bin/release bin/relwithdebinfo bin/minsizerel release) - find_file(OGRE_${COMPONENT}_BINARY_DBG NAMES "Ogre${COMPONENT}_d.dll" HINTS ${OGRE_COMPONENT_SEARCH_PATH_DBG} PATH_SUFFIXES "" bin bin/debug debug) - endif() - mark_as_advanced(OGRE_${COMPONENT}_BINARY_REL OGRE_${COMPONENT}_BINARY_DBG) + if (WIN32) + find_file(OGRE_${COMPONENT}_BINARY_REL NAMES "Ogre${COMPONENT}.dll" HINTS ${OGRE_COMPONENT_SEARCH_PATH_REL} PATH_SUFFIXES "" bin bin/release bin/relwithdebinfo bin/minsizerel release) + find_file(OGRE_${COMPONENT}_BINARY_DBG NAMES "Ogre${COMPONENT}_d.dll" HINTS ${OGRE_COMPONENT_SEARCH_PATH_DBG} PATH_SUFFIXES "" bin bin/debug debug) + endif() + mark_as_advanced(OGRE_${COMPONENT}_BINARY_REL OGRE_${COMPONENT}_BINARY_DBG) endif() endif() endmacro() # look for Paging component ogre_find_component(Paging OgrePaging.h) +# look for Overlay component +ogre_find_component(Overlay OgreOverlaySystem.h) # look for Terrain component ogre_find_component(Terrain OgreTerrain.h) # look for Property component @@ -389,17 +400,17 @@ macro(ogre_find_plugin PLUGIN HEADER) set(TMP_CMAKE_LIB_PREFIX ${CMAKE_FIND_LIBRARY_PREFIXES}) set(CMAKE_FIND_LIBRARY_PREFIXES ${CMAKE_FIND_LIBRARY_PREFIXES} "") endif() - + # strip RenderSystem_ or Plugin_ prefix from plugin name string(REPLACE "RenderSystem_" "" PLUGIN_TEMP ${PLUGIN}) string(REPLACE "Plugin_" "" PLUGIN_NAME ${PLUGIN_TEMP}) - + # header files for plugins are not usually needed, but find them anyway if they are present set(OGRE_PLUGIN_PATH_SUFFIXES - PlugIns PlugIns/${PLUGIN_NAME} Plugins Plugins/${PLUGIN_NAME} ${PLUGIN} + PlugIns PlugIns/${PLUGIN_NAME} Plugins Plugins/${PLUGIN_NAME} ${PLUGIN} RenderSystems RenderSystems/${PLUGIN_NAME} ${ARGN}) - find_path(OGRE_${PLUGIN}_INCLUDE_DIR NAMES ${HEADER} - HINTS ${OGRE_INCLUDE_DIRS} ${OGRE_PREFIX_SOURCE} + find_path(OGRE_${PLUGIN}_INCLUDE_DIR NAMES ${HEADER} + HINTS ${OGRE_INCLUDE_DIRS} ${OGRE_PREFIX_SOURCE} PATH_SUFFIXES ${OGRE_PLUGIN_PATH_SUFFIXES}) # find link libraries for plugins set(OGRE_${PLUGIN}_LIBRARY_NAMES "${PLUGIN}${OGRE_LIB_SUFFIX}") @@ -437,15 +448,15 @@ macro(ogre_find_plugin PLUGIN HEADER) if (OGRE_${PLUGIN}_FOUND) if (NOT OGRE_PLUGIN_DIR_REL OR NOT OGRE_PLUGIN_DIR_DBG) if (WIN32) - set(OGRE_PLUGIN_SEARCH_PATH_REL + set(OGRE_PLUGIN_SEARCH_PATH_REL ${OGRE_LIBRARY_DIR_REL}/.. ${OGRE_LIBRARY_DIR_REL}/../.. - ${OGRE_BIN_SEARCH_PATH} + ${OGRE_BIN_SEARCH_PATH} ) set(OGRE_PLUGIN_SEARCH_PATH_DBG ${OGRE_LIBRARY_DIR_DBG}/.. ${OGRE_LIBRARY_DIR_DBG}/../.. - ${OGRE_BIN_SEARCH_PATH} + ${OGRE_BIN_SEARCH_PATH} ) find_path(OGRE_PLUGIN_DIR_REL NAMES "${PLUGIN}.dll" HINTS ${OGRE_PLUGIN_SEARCH_PATH_REL} PATH_SUFFIXES "" bin bin/release bin/relwithdebinfo bin/minsizerel release) @@ -462,16 +473,16 @@ macro(ogre_find_plugin PLUGIN HEADER) set(OGRE_PLUGIN_DIR_DBG ${OGRE_PLUGIN_DIR_TMP}) endif () endif () - - # find binaries - if (NOT OGRE_STATIC) - if (WIN32) - find_file(OGRE_${PLUGIN}_REL NAMES "${PLUGIN}.dll" HINTS ${OGRE_PLUGIN_DIR_REL}) - find_file(OGRE_${PLUGIN}_DBG NAMES "${PLUGIN}_d.dll" HINTS ${OGRE_PLUGIN_DIR_DBG}) - endif() - mark_as_advanced(OGRE_${PLUGIN}_REL OGRE_${PLUGIN}_DBG) - endif() - + + # find binaries + if (NOT OGRE_STATIC) + if (WIN32) + find_file(OGRE_${PLUGIN}_REL NAMES "${PLUGIN}.dll" HINTS ${OGRE_PLUGIN_DIR_REL}) + find_file(OGRE_${PLUGIN}_DBG NAMES "${PLUGIN}_d.dll" HINTS ${OGRE_PLUGIN_DIR_DBG}) + endif() + mark_as_advanced(OGRE_${PLUGIN}_REL OGRE_${PLUGIN}_DBG) + endif() + endif () if (TMP_CMAKE_LIB_PREFIX) @@ -486,7 +497,7 @@ ogre_find_plugin(Plugin_CgProgramManager OgreCgProgram.h PlugIns/CgProgramManage ogre_find_plugin(Plugin_OctreeSceneManager OgreOctreeSceneManager.h PlugIns/OctreeSceneManager/include) ogre_find_plugin(Plugin_ParticleFX OgreParticleFXPrerequisites.h PlugIns/ParticleFX/include) ogre_find_plugin(RenderSystem_GL OgreGLRenderSystem.h RenderSystems/GL/include) -ogre_find_plugin(RenderSystem_GLES OgreGLESRenderSystem.h RenderSystems/GLES/include) +ogre_find_plugin(RenderSystem_GLES2 OgreGLES2RenderSystem.h RenderSystems/GLES2/include) ogre_find_plugin(RenderSystem_Direct3D9 OgreD3D9RenderSystem.h RenderSystems/Direct3D9/include) ogre_find_plugin(RenderSystem_Direct3D10 OgreD3D10RenderSystem.h RenderSystems/Direct3D10/include) ogre_find_plugin(RenderSystem_Direct3D11 OgreD3D11RenderSystem.h RenderSystems/Direct3D11/include) @@ -515,7 +526,7 @@ if (OGRE_STATIC) if (NOT Cg_FOUND) set(OGRE_Plugin_CgProgramManager_FOUND FALSE) endif () - + set(OGRE_RenderSystem_Direct3D9_LIBRARIES ${OGRE_RenderSystem_Direct3D9_LIBRARIES} ${DirectX9_LIBRARIES} ) @@ -528,8 +539,8 @@ if (OGRE_STATIC) set(OGRE_RenderSystem_GL_LIBRARIES ${OGRE_RenderSystem_GL_LIBRARIES} ${OPENGL_LIBRARIES} ) - set(OGRE_RenderSystem_GLES_LIBRARIES ${OGRE_RenderSystem_GLES_LIBRARIES} - ${OPENGLES_LIBRARIES} + set(OGRE_RenderSystem_GLES2_LIBRARIES ${OGRE_RenderSystem_GLES2_LIBRARIES} + ${OPENGLES2_LIBRARIES} ) set(OGRE_Plugin_CgProgramManager_LIBRARIES ${OGRE_Plugin_CgProgramManager_LIBRARIES} ${Cg_LIBRARIES} diff --git a/cmake/GetGitRevisionDescription.cmake b/cmake/GetGitRevisionDescription.cmake deleted file mode 100644 index 56ff1d545..000000000 --- a/cmake/GetGitRevisionDescription.cmake +++ /dev/null @@ -1,154 +0,0 @@ -# - Returns a version string from Git -# -# These functions force a re-configure on each git commit so that you can -# trust the values of the variables in your build system. -# -# get_git_head_revision( [ ...]) -# -# Returns the refspec and sha hash of the current head revision -# -# git_describe( [ ...]) -# -# Returns the results of git describe on the source tree, and adjusting -# the output so that it tests false if an error occurs. -# -# git_get_exact_tag( [ ...]) -# -# Returns the results of git describe --exact-match on the source tree, -# and adjusting the output so that it tests false if there was no exact -# matching tag. -# -# Requires CMake 2.6 or newer (uses the 'function' command) -# -# Original Author: -# 2009-2010 Ryan Pavlik -# http://academic.cleardefinition.com -# Iowa State University HCI Graduate Program/VRAC -# -# Copyright Iowa State University 2009-2010. -# Distributed under the Boost Software License, Version 1.0. -# (See accompanying file LICENSE_1_0.txt or copy at -# http://www.boost.org/LICENSE_1_0.txt) - -if(__get_git_revision_description) - return() -endif() -set(__get_git_revision_description YES) - -# We must run the following at "include" time, not at function call time, -# to find the path to this module rather than the path to a calling list file -get_filename_component(_gitdescmoddir ${CMAKE_CURRENT_LIST_FILE} PATH) - -function(get_git_head_revision _refspecvar _hashvar) - set(GIT_PARENT_DIR "${CMAKE_CURRENT_SOURCE_DIR}") - set(GIT_DIR "${GIT_PARENT_DIR}/.git") - while(NOT EXISTS "${GIT_DIR}") # .git dir not found, search parent directories - set(GIT_PREVIOUS_PARENT "${GIT_PARENT_DIR}") - get_filename_component(GIT_PARENT_DIR ${GIT_PARENT_DIR} PATH) - if(GIT_PARENT_DIR STREQUAL GIT_PREVIOUS_PARENT) - # We have reached the root directory, we are not in git - set(${_refspecvar} "GITDIR-NOTFOUND" PARENT_SCOPE) - set(${_hashvar} "GITDIR-NOTFOUND" PARENT_SCOPE) - return() - endif() - - set(GIT_DIR "${GIT_PARENT_DIR}/.git") - endwhile() - - # check if this is a submodule - if(NOT IS_DIRECTORY ${GIT_DIR}) - file(READ ${GIT_DIR} submodule) - string(REGEX REPLACE "gitdir: (.*)\n$" "\\1" GIT_DIR_RELATIVE ${submodule}) - get_filename_component(SUBMODULE_DIR ${GIT_DIR} PATH) - get_filename_component(GIT_DIR ${SUBMODULE_DIR}/${GIT_DIR_RELATIVE} ABSOLUTE) - endif() - - set(GIT_DATA "${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/git-data") - - if(NOT EXISTS "${GIT_DATA}") - file(MAKE_DIRECTORY "${GIT_DATA}") - endif() - - if(NOT EXISTS "${GIT_DIR}/HEAD") - return() - endif() - - set(HEAD_FILE "${GIT_DATA}/HEAD") - configure_file("${GIT_DIR}/HEAD" "${HEAD_FILE}" COPYONLY) - - configure_file("${_gitdescmoddir}/GetGitRevisionDescription.cmake.in" - "${GIT_DATA}/grabRef.cmake" @ONLY) - include("${GIT_DATA}/grabRef.cmake") - - set(${_refspecvar} "${HEAD_REF}" PARENT_SCOPE) - set(${_hashvar} "${HEAD_HASH}" PARENT_SCOPE) -endfunction() - -function(git_describe _var) - #get_git_head_revision(refspec hash) - - if(NOT GIT_FOUND) - set(${_var} "GIT-NOTFOUND" PARENT_SCOPE) - return() - endif() - - #if(NOT hash) - # set(${_var} "HEAD-HASH-NOTFOUND" PARENT_SCOPE) - # return() - #endif() - - # TODO sanitize - #if((${ARGN}" MATCHES "&&") OR - # (ARGN MATCHES "||") OR - # (ARGN MATCHES "\\;")) - # message("Please report the following error to the project!") - # message(FATAL_ERROR "Looks like someone's doing something nefarious with git_describe! Passed arguments ${ARGN}") - #endif() - - #message(STATUS "Arguments to execute_process: ${ARGN}") - - execute_process(COMMAND - "${GIT_EXECUTABLE}" - describe - #${hash} - ${ARGN} - WORKING_DIRECTORY - "${CMAKE_SOURCE_DIR}" - RESULT_VARIABLE - res - OUTPUT_VARIABLE - out - OUTPUT_STRIP_TRAILING_WHITESPACE) - - if(NOT res EQUAL 0) - set(out "${out}-${res}-NOTFOUND") - endif() - - set(${_var} "${out}" PARENT_SCOPE) -endfunction() - -function(get_git_tag_revision _var) - if(NOT GIT_FOUND) - set(${_var} "GIT-NOTFOUND" PARENT_SCOPE) - return() - endif() - - execute_process(COMMAND - "${GIT_EXECUTABLE}" - rev-list - ${ARGN} - WORKING_DIRECTORY - "${CMAKE_SOURCE_DIR}" - RESULT_VARIABLE - res - OUTPUT_VARIABLE - out - OUTPUT_STRIP_TRAILING_WHITESPACE) - if(NOT res EQUAL 0) - set(out "${out}-${res}-NOTFOUND") - endif() - - set(${_var} "${out}" PARENT_SCOPE) -endfunction() - - diff --git a/cmake/GetGitRevisionDescription.cmake.in b/cmake/GetGitRevisionDescription.cmake.in deleted file mode 100644 index 888ce13aa..000000000 --- a/cmake/GetGitRevisionDescription.cmake.in +++ /dev/null @@ -1,38 +0,0 @@ -# -# Internal file for GetGitRevisionDescription.cmake -# -# Requires CMake 2.6 or newer (uses the 'function' command) -# -# Original Author: -# 2009-2010 Ryan Pavlik -# http://academic.cleardefinition.com -# Iowa State University HCI Graduate Program/VRAC -# -# Copyright Iowa State University 2009-2010. -# Distributed under the Boost Software License, Version 1.0. -# (See accompanying file LICENSE_1_0.txt or copy at -# http://www.boost.org/LICENSE_1_0.txt) - -set(HEAD_HASH) - -file(READ "@HEAD_FILE@" HEAD_CONTENTS LIMIT 1024) - -string(STRIP "${HEAD_CONTENTS}" HEAD_CONTENTS) -if(HEAD_CONTENTS MATCHES "ref") - # named branch - string(REPLACE "ref: " "" HEAD_REF "${HEAD_CONTENTS}") - if(EXISTS "@GIT_DIR@/${HEAD_REF}") - configure_file("@GIT_DIR@/${HEAD_REF}" "@GIT_DATA@/head-ref" COPYONLY) - elseif(EXISTS "@GIT_DIR@/logs/${HEAD_REF}") - configure_file("@GIT_DIR@/logs/${HEAD_REF}" "@GIT_DATA@/head-ref" COPYONLY) - set(HEAD_HASH "${HEAD_REF}") - endif() -else() - # detached HEAD - configure_file("@GIT_DIR@/HEAD" "@GIT_DATA@/head-ref" COPYONLY) -endif() - -if(NOT HEAD_HASH) - file(READ "@GIT_DATA@/head-ref" HEAD_HASH LIMIT 1024) - string(STRIP "${HEAD_HASH}" HEAD_HASH) -endif() diff --git a/cmake/GitVersion.cmake b/cmake/GitVersion.cmake new file mode 100644 index 000000000..0087461a1 --- /dev/null +++ b/cmake/GitVersion.cmake @@ -0,0 +1,24 @@ +execute_process ( + COMMAND ${GIT_EXECUTABLE} rev-list --tags --max-count=1 + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + RESULT_VARIABLE EXITCODE1 + OUTPUT_VARIABLE TAGHASH + OUTPUT_STRIP_TRAILING_WHITESPACE) + +execute_process ( + COMMAND ${GIT_EXECUTABLE} rev-parse HEAD + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + RESULT_VARIABLE EXITCODE2 + OUTPUT_VARIABLE COMMITHASH + OUTPUT_STRIP_TRAILING_WHITESPACE) + +string (COMPARE EQUAL "${EXITCODE1}:${EXITCODE2}" "0:0" SUCCESS) +if (SUCCESS) + set(OPENMW_VERSION_COMMITHASH "${COMMITHASH}") + set(OPENMW_VERSION_TAGHASH "${TAGHASH}") + message(STATUS "OpenMW version ${OPENMW_VERSION}") +else (SUCCESS) + message(WARNING "Failed to get valid version information from Git") +endif (SUCCESS) + +configure_file(${VERSION_HPP_IN} ${VERSION_HPP}) diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index 8f9fb17af..a49e54dd3 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -1,8 +1,24 @@ project (Components) -set (CMAKE_BUILD_TYPE DEBUG) # Version file -configure_file("${CMAKE_CURRENT_SOURCE_DIR}/version/version.hpp.cmake" "${CMAKE_CURRENT_SOURCE_DIR}/version/version.hpp") +set (VERSION_HPP_IN ${CMAKE_CURRENT_SOURCE_DIR}/version/version.hpp.cmake) +set (VERSION_HPP ${CMAKE_CURRENT_SOURCE_DIR}/version/version.hpp) +if (GIT_CHECKOUT) + add_custom_target (git-version + COMMAND ${CMAKE_COMMAND} + -DGIT_EXECUTABLE=${GIT_EXECUTABLE} + -DPROJECT_SOURCE_DIR=${PROJECT_SOURCE_DIR} + -DVERSION_HPP_IN=${VERSION_HPP_IN} + -DVERSION_HPP=${VERSION_HPP} + -DOPENMW_VERSION_MAJOR=${OPENMW_VERSION_MAJOR} + -DOPENMW_VERSION_MINOR=${OPENMW_VERSION_MINOR} + -DOPENMW_VERSION_RELEASE=${OPENMW_VERSION_RELEASE} + -DOPENMW_VERSION=${OPENMW_VERSION} + -P ${CMAKE_CURRENT_SOURCE_DIR}/../cmake/GitVersion.cmake + VERBATIM) +else (GIT_CHECKOUT) + configure_file(${VERSION_HPP_IN} ${VERSION_HPP}) +endif (GIT_CHECKOUT) # source files @@ -19,7 +35,11 @@ add_component_dir (bsa ) add_component_dir (nif - controlled effect niftypes record controller extra node record_ptr data niffile property + controlled effect niftypes record controller extra node record_ptr data niffile property nifkey data node base nifstream + ) + +add_component_dir (nifcache + nifcache ) add_component_dir (nifogre @@ -36,21 +56,29 @@ add_component_dir (to_utf8 add_component_dir (esm attr defs esmcommon esmreader esmwriter loadacti loadalch loadappa loadarmo loadbody loadbook loadbsgn loadcell - loadclas loadclot loadcont loadcrea loadcrec loaddial loaddoor loadench loadfact loadglob loadgmst - loadinfo loadingr loadland loadlevlist loadligh loadlock loadprob loadrepa loadltex loadmgef loadmisc loadnpcc + loadclas loadclot loadcont loadcrea loaddial loaddoor loadench loadfact loadglob loadgmst + loadinfo loadingr loadland loadlevlist loadligh loadlock loadprob loadrepa loadltex loadmgef loadmisc loadnpc loadpgrd loadrace loadregn loadscpt loadskil loadsndg loadsoun loadspel loadsscr loadstat loadweap records aipackage effectlist spelllist variant variantimp loadtes3 cellref filter - savedgame journalentry queststate locals globalscript player objectstate cellid cellstate globalmap lightstate inventorystate containerstate npcstate creaturestate dialoguestate statstate - npcstats creaturestats weatherstate quickkeys fogstate spellstate activespells creaturelevliststate doorstate projectilestate - aisequence + savedgame journalentry queststate locals globalscript player objectstate cellid cellstate globalmap inventorystate containerstate npcstate creaturestate dialoguestate statstate + npcstats creaturestats weatherstate quickkeys fogstate spellstate activespells creaturelevliststate doorstate projectilestate debugprofile + aisequence magiceffects util custommarkerstate stolenitems + ) + +add_component_dir (esmterrain + storage ) add_component_dir (misc - utf8stream stringops + utf8stream stringops resourcehelpers ) +IF(NOT WIN32 AND NOT APPLE) + add_definitions(-DGLOBAL_DATA_PATH="${GLOBAL_DATA_PATH}") + add_definitions(-DGLOBAL_CONFIG_PATH="${GLOBAL_CONFIG_PATH}") +ENDIF() add_component_dir (files - linuxpath windowspath macospath fixedpath multidircollection collections configurationmanager + linuxpath androidpath windowspath macospath fixedpath multidircollection collections configurationmanager constrainedfiledatastream lowlevelfile ) @@ -58,7 +86,7 @@ add_component_dir (compiler context controlparser errorhandler exception exprparser extensions fileparser generator lineparser literals locals output parser scanner scriptparser skipparser streamerrorhandler stringparser tokenloc nullerrorhandler opcodes extensions0 declarationparser - quickfileparser + quickfileparser discardparser junkparser ) add_component_dir (interpreter @@ -72,7 +100,7 @@ add_component_dir (translation add_definitions(-DTERRAIN_USE_SHADER=1) add_component_dir (terrain - quadtreenode chunk world defaultworld storage material buffercache defs + quadtreenode chunk world defaultworld terraingrid storage material buffercache defs ) add_component_dir (loadinglistener @@ -83,6 +111,14 @@ add_component_dir (ogreinit ogreinit ogreplugin ) +add_component_dir (widgets + box imagebutton tags list numericeditbox sharedstatebutton windowcaption widgets + ) + +add_component_dir (fontloader + fontloader + ) + add_component_dir (version version ) @@ -93,23 +129,43 @@ set (ESM_UI ${CMAKE_SOURCE_DIR}/files/ui/contentselector.ui find_package(Qt4 COMPONENTS QtCore QtGui) if(QT_QTGUI_LIBRARY AND QT_QTCORE_LIBRARY) - add_component_qt_dir (contentselector + add_component_qt_dir (contentselector model/modelitem model/esmfile model/naturalsort model/contentmodel + model/loadordererror view/combobox view/contentselector - ) + ) + add_component_qt_dir (config + gamesettings + launchersettings + settingsbase + ) + + add_component_qt_dir (process + processinvoker + ) include(${QT_USE_FILE}) QT4_WRAP_UI(ESM_UI_HDR ${ESM_UI}) QT4_WRAP_CPP(MOC_SRCS ${COMPONENT_MOC_FILES}) endif(QT_QTGUI_LIBRARY AND QT_QTCORE_LIBRARY) +if (CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") + if("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "x86_64" AND NOT APPLE) + add_definitions(-fPIC) + endif() +endif () + include_directories(${BULLET_INCLUDE_DIRS} ${CMAKE_CURRENT_BINARY_DIR}) add_library(components STATIC ${COMPONENT_FILES} ${MOC_SRCS} ${ESM_UI_HDR}) target_link_libraries(components ${Boost_LIBRARIES} ${OGRE_LIBRARIES}) +if (GIT_CHECKOUT) + add_dependencies (components git-version) +endif (GIT_CHECKOUT) + # Fix for not visible pthreads functions for linker with glibc 2.15 if (UNIX AND NOT APPLE) target_link_libraries(components ${CMAKE_THREAD_LIBS_INIT}) diff --git a/components/bsa/bsa_archive.cpp b/components/bsa/bsa_archive.cpp index 6574f096b..4f656f9c4 100644 --- a/components/bsa/bsa_archive.cpp +++ b/components/bsa/bsa_archive.cpp @@ -45,7 +45,7 @@ static char strict_normalize_char(char ch) static char nonstrict_normalize_char(char ch) { - return ch == '\\' ? '/' : std::tolower(ch); + return ch == '\\' ? '/' : std::tolower(ch,std::locale::classic()); } template diff --git a/components/bsa/tests/.gitignore b/components/bsa/tests/.gitignore deleted file mode 100644 index e2f2f332d..000000000 --- a/components/bsa/tests/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*_test -bsatool -*.bsa diff --git a/components/bsa/tests/Makefile b/components/bsa/tests/Makefile deleted file mode 100644 index 73e20d7b3..000000000 --- a/components/bsa/tests/Makefile +++ /dev/null @@ -1,15 +0,0 @@ -GCC=g++ - -all: bsa_file_test ogre_archive_test - -I_OGRE=$(shell pkg-config --cflags OGRE) -L_OGRE=$(shell pkg-config --libs OGRE) - -bsa_file_test: bsa_file_test.cpp ../bsa_file.cpp - $(GCC) $^ -o $@ - -ogre_archive_test: ogre_archive_test.cpp ../bsa_file.cpp ../bsa_archive.cpp - $(GCC) $^ -o $@ $(I_OGRE) $(L_OGRE) - -clean: - rm *_test diff --git a/components/bsa/tests/bsa_file_test.cpp b/components/bsa/tests/bsa_file_test.cpp deleted file mode 100644 index 07ee73d17..000000000 --- a/components/bsa/tests/bsa_file_test.cpp +++ /dev/null @@ -1,44 +0,0 @@ -#include "../bsa_file.hpp" - -/* - Test of the BSAFile class - - This test requires that data/Morrowind.bsa exists in the root - directory of OpenMW. - - */ - -#include - -using namespace std; -using namespace Bsa; - -BSAFile bsa; - -void find(const char* file) -{ - cout << "Does file '" << file << "' exist?\n "; - if(bsa.exists(file)) - { - cout << "Yes.\n "; - cout << bsa.getFile(file)->size() << " bytes\n"; - } - else cout << "No.\n"; -} - -int main() -{ - cout << "Reading Morrowind.bsa\n"; - bsa.open("../../data/Morrowind.bsa"); - - const BSAFile::FileList &files = bsa.getList(); - - cout << "First 10 files in archive:\n"; - for(int i=0; i<10; i++) - cout << " " << files[i].name - << " (" << files[i].fileSize << " bytes @" - << files[i].offset << ")\n"; - - find("meshes\\r\\xnetch_betty.nif"); - find("humdrum"); -} diff --git a/components/bsa/tests/ogre_archive_test.cpp b/components/bsa/tests/ogre_archive_test.cpp deleted file mode 100644 index 9cd57240f..000000000 --- a/components/bsa/tests/ogre_archive_test.cpp +++ /dev/null @@ -1,34 +0,0 @@ -#include -#include - -// This is a test of the BSA archive handler for OGRE. - -#include "../bsa_archive.hpp" - -using namespace Ogre; -using namespace std; - -int main() -{ - // Disable Ogre logging - new LogManager; - Log *log = LogManager::getSingleton().createLog(""); - log->setDebugOutputEnabled(false); - - // Set up Root - Root *root = new Root("","",""); - - // Add the BSA - Bsa::addBSA("../../data/Morrowind.bsa"); - - // Pick a sample file - String tex = "textures\\tx_natural_cavern_wall13.dds"; - cout << "Opening file: " << tex << endl; - - // Get it from the resource system - DataStreamPtr data = ResourceGroupManager::getSingleton().openResource(tex, "General"); - - cout << "Size: " << data->size() << endl; - - return 0; -} diff --git a/components/bsa/tests/output/bsa_file_test.out b/components/bsa/tests/output/bsa_file_test.out deleted file mode 100644 index 0d8e76cdd..000000000 --- a/components/bsa/tests/output/bsa_file_test.out +++ /dev/null @@ -1,17 +0,0 @@ -Reading Morrowind.bsa -First 10 files in archive: - meshes\m\probe_journeyman_01.nif (6276 bytes @126646052) - textures\menu_rightbuttonup_top.dds (256 bytes @218530052) - textures\menu_rightbuttonup_right.dds (256 bytes @218529796) - textures\menu_rightbuttonup_left.dds (256 bytes @218529540) - textures\menu_rightbuttondown_top.dds (256 bytes @218528196) - meshes\b\b_n_redguard_f_skins.nif (41766 bytes @17809778) - meshes\b\b_n_redguard_m_skins.nif (41950 bytes @18103107) - meshes\b\b_n_redguard_f_wrist.nif (2355 bytes @17858132) - meshes\b\b_n_redguard_m_foot.nif (4141 bytes @17862081) - meshes\b\b_n_redguard_m_knee.nif (2085 bytes @18098101) -Does file 'meshes\r\xnetch_betty.nif' exist? - Yes. - 53714 bytes -Does file 'humdrum' exist? - No. diff --git a/components/bsa/tests/output/ogre_archive_test.out b/components/bsa/tests/output/ogre_archive_test.out deleted file mode 100644 index 748e4b1e7..000000000 --- a/components/bsa/tests/output/ogre_archive_test.out +++ /dev/null @@ -1,2 +0,0 @@ -Opening file: textures\tx_natural_cavern_wall13.dds -Size: 43808 diff --git a/components/bsa/tests/test.sh b/components/bsa/tests/test.sh deleted file mode 100755 index 2d07708ad..000000000 --- a/components/bsa/tests/test.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash - -make || exit - -mkdir -p output - -PROGS=*_test - -for a in $PROGS; do - if [ -f "output/$a.out" ]; then - echo "Running $a:" - ./$a | diff output/$a.out - - else - echo "Creating $a.out" - ./$a > "output/$a.out" - git add "output/$a.out" - fi -done diff --git a/components/compiler/discardparser.cpp b/components/compiler/discardparser.cpp new file mode 100644 index 000000000..6028968bb --- /dev/null +++ b/components/compiler/discardparser.cpp @@ -0,0 +1,70 @@ + +#include "discardparser.hpp" + +#include "scanner.hpp" + +namespace Compiler +{ + DiscardParser::DiscardParser (ErrorHandler& errorHandler, const Context& context) + : Parser (errorHandler, context), mState (StartState) + { + + } + + bool DiscardParser::parseInt (int value, const TokenLoc& loc, Scanner& scanner) + { + if (mState==StartState || mState==CommaState || mState==MinusState) + { + start(); + return false; + } + + return Parser::parseInt (value, loc, scanner); + } + + bool DiscardParser::parseFloat (float value, const TokenLoc& loc, Scanner& scanner) + { + if (mState==StartState || mState==CommaState || mState==MinusState) + { + start(); + return false; + } + + return Parser::parseFloat (value, loc, scanner); + } + + bool DiscardParser::parseName (const std::string& name, const TokenLoc& loc, + Scanner& scanner) + { + if (mState==StartState || mState==CommaState) + { + start(); + return false; + } + + return Parser::parseName (name, loc, scanner); + } + + bool DiscardParser::parseSpecial (int code, const TokenLoc& loc, Scanner& scanner) + { + if (code==Scanner::S_comma && mState==StartState) + { + mState = CommaState; + return true; + } + + if (code==Scanner::S_minus && (mState==StartState || mState==CommaState)) + { + mState = MinusState; + return true; + } + + return Parser::parseSpecial (code, loc, scanner); + } + + void DiscardParser::reset() + { + mState = StartState; + Parser::reset(); + } +} diff --git a/components/compiler/discardparser.hpp b/components/compiler/discardparser.hpp new file mode 100644 index 000000000..bee8a87bb --- /dev/null +++ b/components/compiler/discardparser.hpp @@ -0,0 +1,45 @@ +#ifndef COMPILER_DISCARDPARSER_H_INCLUDED +#define COMPILER_DISCARDPARSER_H_INCLUDED + +#include "parser.hpp" + +namespace Compiler +{ + /// \brief Parse a single optional numeric value or string and discard it + class DiscardParser : public Parser + { + enum State + { + StartState, CommaState, MinusState + }; + + State mState; + + public: + + DiscardParser (ErrorHandler& errorHandler, const Context& context); + + virtual bool parseInt (int value, const TokenLoc& loc, Scanner& scanner); + ///< Handle an int token. + /// \return fetch another token? + + virtual bool parseFloat (float value, const TokenLoc& loc, Scanner& scanner); + ///< Handle a float token. + /// \return fetch another token? + + virtual bool parseName (const std::string& name, const TokenLoc& loc, + Scanner& scanner); + ///< Handle a name token. + /// \return fetch another token? + + virtual bool parseSpecial (int code, const TokenLoc& loc, Scanner& scanner); + ///< Handle a special character token. + /// \return fetch another token? + + virtual void reset(); + ///< Reset parser to clean state. + }; +} + +#endif + diff --git a/components/compiler/errorhandler.cpp b/components/compiler/errorhandler.cpp index fe58836cc..bcd30ef2d 100644 --- a/components/compiler/errorhandler.cpp +++ b/components/compiler/errorhandler.cpp @@ -3,11 +3,8 @@ namespace Compiler { - // constructor - - ErrorHandler::ErrorHandler() : mWarnings (0), mErrors (0), mWarningsMode (1) {} - - // destructor + ErrorHandler::ErrorHandler() + : mWarnings (0), mErrors (0), mWarningsMode (1), mDowngradeErrors (false) {} ErrorHandler::~ErrorHandler() {} @@ -49,6 +46,12 @@ namespace Compiler void ErrorHandler::error (const std::string& message, const TokenLoc& loc) { + if (mDowngradeErrors) + { + warning (message, loc); + return; + } + ++mErrors; report (message, loc, ErrorMessage); } @@ -72,4 +75,21 @@ namespace Compiler { mWarningsMode = mode; } + + void ErrorHandler::downgradeErrors (bool downgrade) + { + mDowngradeErrors = downgrade; + } + + + ErrorDowngrade::ErrorDowngrade (ErrorHandler& handler) : mHandler (handler) + { + mHandler.downgradeErrors (true); + } + + ErrorDowngrade::~ErrorDowngrade() + { + mHandler.downgradeErrors (false); + } + } diff --git a/components/compiler/errorhandler.hpp b/components/compiler/errorhandler.hpp index e5922a6be..c92e7bb8d 100644 --- a/components/compiler/errorhandler.hpp +++ b/components/compiler/errorhandler.hpp @@ -17,6 +17,7 @@ namespace Compiler int mWarnings; int mErrors; int mWarningsMode; + bool mDowngradeErrors; protected: @@ -66,6 +67,26 @@ namespace Compiler void setWarningsMode (int mode); ///< // 0 ignore, 1 rate as warning, 2 rate as error + + /// Treat errors as warnings. + void downgradeErrors (bool downgrade); + }; + + class ErrorDowngrade + { + ErrorHandler& mHandler; + + /// not implemented + ErrorDowngrade (const ErrorDowngrade&); + + /// not implemented + ErrorDowngrade& operator= (const ErrorDowngrade&); + + public: + + ErrorDowngrade (ErrorHandler& handler); + + ~ErrorDowngrade(); }; } diff --git a/components/compiler/exprparser.cpp b/components/compiler/exprparser.cpp index e54b2e2a8..7eaca5c02 100644 --- a/components/compiler/exprparser.cpp +++ b/components/compiler/exprparser.cpp @@ -16,6 +16,8 @@ #include "stringparser.hpp" #include "extensions.hpp" #include "context.hpp" +#include "discardparser.hpp" +#include "junkparser.hpp" namespace Compiler { @@ -386,6 +388,9 @@ namespace Compiler mExplicit.clear(); mRefOp = false; + std::vector ignore; + parseArguments ("x", scanner, ignore); + mNextOperand = false; return true; } @@ -404,6 +409,21 @@ namespace Compiler mNextOperand = false; return true; } + else if (keyword==Scanner::K_scriptrunning) + { + start(); + + mTokenLoc = loc; + parseArguments ("c", scanner); + + Generator::scriptRunning (mCode); + mOperands.push_back ('l'); + + mExplicit.clear(); + mRefOp = false; + mNextOperand = false; + return true; + } // check for custom extensions if (const Extensions *extensions = getContext().getExtensions()) @@ -527,6 +547,9 @@ namespace Compiler Generator::getDisabled (mCode, mLiterals, ""); mOperands.push_back ('l'); + std::vector ignore; + parseArguments ("x", scanner, ignore); + mNextOperand = false; return true; } @@ -638,7 +661,7 @@ namespace Compiler else { // no comma was used between arguments - scanner.putbackKeyword (code, loc); + scanner.putbackSpecial (code, loc); return false; } } @@ -730,13 +753,15 @@ namespace Compiler } int ExprParser::parseArguments (const std::string& arguments, Scanner& scanner, - std::vector& code) + std::vector& code, int ignoreKeyword) { bool optional = false; int optionalCount = 0; ExprParser parser (getErrorHandler(), getContext(), mLocals, mLiterals, true); StringParser stringParser (getErrorHandler(), getContext(), mLiterals); + DiscardParser discardParser (getErrorHandler(), getContext()); + JunkParser junkParser (getErrorHandler(), getContext(), ignoreKeyword); std::stack > stack; @@ -771,11 +796,39 @@ namespace Compiler ++optionalCount; } } + else if (*iter=='X') + { + parser.reset(); + + parser.setOptional (true); + + scanner.scan (parser); + + if (parser.isEmpty()) + break; + } + else if (*iter=='z') + { + discardParser.reset(); + discardParser.setOptional (true); + + scanner.scan (discardParser); + + if (discardParser.isEmpty()) + break; + } + else if (*iter=='j') + { + /// \todo disable this when operating in strict mode + junkParser.reset(); + + scanner.scan (junkParser); + } else { parser.reset(); - if (optional || *iter == 'X') + if (optional) parser.setOptional (true); scanner.scan (parser); @@ -783,20 +836,17 @@ namespace Compiler if (optional && parser.isEmpty()) break; - if (*iter != 'X') - { - std::vector tmp; + std::vector tmp; - char type = parser.append (tmp); + char type = parser.append (tmp); - if (type!=*iter) - Generator::convert (tmp, type, *iter); + if (type!=*iter) + Generator::convert (tmp, type, *iter); - stack.push (tmp); + stack.push (tmp); - if (optional) - ++optionalCount; - } + if (optional) + ++optionalCount; } } diff --git a/components/compiler/exprparser.hpp b/components/compiler/exprparser.hpp index e4e385ff0..639ca65aa 100644 --- a/components/compiler/exprparser.hpp +++ b/components/compiler/exprparser.hpp @@ -96,11 +96,12 @@ namespace Compiler /// \return Type ('l': integer, 'f': float) int parseArguments (const std::string& arguments, Scanner& scanner, - std::vector& code); + std::vector& code, int ignoreKeyword = -1); ///< Parse sequence of arguments specified by \a arguments. /// \param arguments Uses ScriptArgs typedef /// \see Compiler::ScriptArgs /// \param invert Store arguments in reverted order. + /// \param ignoreKeyword A keyword that is seen as junk /// \return number of optional arguments }; } diff --git a/components/compiler/extensions.hpp b/components/compiler/extensions.hpp index d229751de..9fb9bdb95 100644 --- a/components/compiler/extensions.hpp +++ b/components/compiler/extensions.hpp @@ -21,7 +21,9 @@ namespace Compiler s - Short
    S - String, case preserved
    x - Optional, ignored string argument - X - Optional, ignored integer argument + X - Optional, ignored numeric expression + z - Optional, ignored string or numeric argument + j - A piece of junk (either . or a specific keyword) **/ typedef std::string ScriptArgs; diff --git a/components/compiler/extensions0.cpp b/components/compiler/extensions0.cpp index ef4fe4fbd..c56ee2ffb 100644 --- a/components/compiler/extensions0.cpp +++ b/components/compiler/extensions0.cpp @@ -113,12 +113,12 @@ namespace Compiler { void registerExtensions (Extensions& extensions) { - extensions.registerInstruction ("additem", "cl", opcodeAddItem, opcodeAddItemExplicit); + extensions.registerInstruction ("additem", "clX", opcodeAddItem, opcodeAddItemExplicit); extensions.registerFunction ("getitemcount", 'l', "c", opcodeGetItemCount, opcodeGetItemCountExplicit); - extensions.registerInstruction ("removeitem", "cl", opcodeRemoveItem, + extensions.registerInstruction ("removeitem", "clX", opcodeRemoveItem, opcodeRemoveItemExplicit); - extensions.registerInstruction ("equip", "c", opcodeEquip, opcodeEquipExplicit); + extensions.registerInstruction ("equip", "cX", opcodeEquip, opcodeEquipExplicit); extensions.registerFunction ("getarmortype", 'l', "l", opcodeGetArmorType, opcodeGetArmorTypeExplicit); extensions.registerFunction ("hasitemequipped", 'l', "c", opcodeHasItemEquipped, opcodeHasItemEquippedExplicit); extensions.registerFunction ("hassoulgem", 'l', "c", opcodeHasSoulGem, opcodeHasSoulGemExplicit); @@ -148,6 +148,16 @@ namespace Compiler extensions.registerInstruction ("forcerun", "", opcodeForceRun, opcodeForceRunExplicit); + extensions.registerInstruction ("clearforcejump", "", opcodeClearForceJump, + opcodeClearForceJumpExplicit); + extensions.registerInstruction ("forcejump", "", opcodeForceJump, + opcodeForceJumpExplicit); + + extensions.registerInstruction ("clearforcemovejump", "", opcodeClearForceMoveJump, + opcodeClearForceMoveJumpExplicit); + extensions.registerInstruction ("forcemovejump", "", opcodeForceMoveJump, + opcodeForceMoveJumpExplicit); + extensions.registerInstruction ("clearforcesneak", "", opcodeClearForceSneak, opcodeClearForceSneakExplicit); extensions.registerInstruction ("forcesneak", "", opcodeForceSneak, @@ -155,6 +165,8 @@ namespace Compiler extensions.registerFunction ("getpcrunning", 'l', "", opcodeGetPcRunning); extensions.registerFunction ("getpcsneaking", 'l', "", opcodeGetPcSneaking); extensions.registerFunction ("getforcerun", 'l', "", opcodeGetForceRun, opcodeGetForceRunExplicit); + extensions.registerFunction ("getforcejump", 'l', "", opcodeGetForceJump, opcodeGetForceJumpExplicit); + extensions.registerFunction ("getforcemovejump", 'l', "", opcodeGetForceMoveJump, opcodeGetForceMoveJumpExplicit); extensions.registerFunction ("getforcesneak", 'l', "", opcodeGetForceSneak, opcodeGetForceSneakExplicit); } } @@ -167,7 +179,7 @@ namespace Compiler extensions.registerInstruction ("setjournalindex", "cl", opcodeSetJournalIndex); extensions.registerFunction ("getjournalindex", 'l', "c", opcodeGetJournalIndex); extensions.registerInstruction ("addtopic", "S" , opcodeAddTopic); - extensions.registerInstruction ("choice", "/SlSlSlSlSlSlSlSlSlSlSlSlSlSlSlSl", opcodeChoice); + extensions.registerInstruction ("choice", "j/SlSlSlSlSlSlSlSlSlSlSlSlSlSlSlSl", opcodeChoice); extensions.registerInstruction("forcegreeting","",opcodeForceGreeting, opcodeForceGreetingExplicit); extensions.registerInstruction("goodbye", "", opcodeGoodbye); @@ -180,6 +192,7 @@ namespace Compiler extensions.registerFunction("samefaction", 'l', "", opcodeSameFaction, opcodeSameFactionExplicit); extensions.registerInstruction("modfactionreaction", "ccl", opcodeModFactionReaction); + extensions.registerInstruction("setfactionreaction", "ccl", opcodeSetFactionReaction); extensions.registerFunction("getfactionreaction", 'l', "ccX", opcodeGetFactionReaction); extensions.registerInstruction("clearinfoactor", "", opcodeClearInfoActor, opcodeClearInfoActorExplicit); } @@ -202,7 +215,7 @@ namespace Compiler extensions.registerInstruction ("enablestatsmenu", "", opcodeEnableStatsMenu); extensions.registerInstruction ("enablerest", "", opcodeEnableRest); - extensions.registerInstruction ("enablelevelupmenu", "", opcodeEnableRest); + extensions.registerInstruction ("enablelevelupmenu", "", opcodeEnableLevelupMenu); extensions.registerInstruction ("showrestmenu", "", opcodeShowRestMenu, opcodeShowRestMenuExplicit); @@ -214,7 +227,7 @@ namespace Compiler extensions.registerInstruction ("togglefullhelp", "", opcodeToggleFullHelp); extensions.registerInstruction ("tfh", "", opcodeToggleFullHelp); - extensions.registerInstruction ("showmap", "S", opcodeShowMap); + extensions.registerInstruction ("showmap", "Sxxxx", opcodeShowMap); extensions.registerInstruction ("fillmap", "", opcodeFillMap); extensions.registerInstruction ("menutest", "/l", opcodeMenuTest); extensions.registerInstruction ("togglemenus", "", opcodeToggleMenus); @@ -228,7 +241,7 @@ namespace Compiler { extensions.registerFunction ("xbox", 'l', "", opcodeXBox); extensions.registerFunction ("onactivate", 'l', "", opcodeOnActivate); - extensions.registerInstruction ("activate", "", opcodeActivate, opcodeActivateExplicit); + extensions.registerInstruction ("activate", "x", opcodeActivate, opcodeActivateExplicit); extensions.registerInstruction ("lock", "/l", opcodeLock, opcodeLockExplicit); extensions.registerInstruction ("unlock", "", opcodeUnlock, opcodeUnlockExplicit); extensions.registerInstruction ("cast", "SS", opcodeCast, opcodeCastExplicit); @@ -244,9 +257,14 @@ namespace Compiler extensions.registerInstruction ("fadeto", "ff", opcodeFadeTo); extensions.registerInstruction ("togglewater", "", opcodeToggleWater); extensions.registerInstruction ("twa", "", opcodeToggleWater); + extensions.registerInstruction ("toggleworld", "", opcodeToggleWorld); + extensions.registerInstruction ("tw", "", opcodeToggleWorld); extensions.registerInstruction ("togglepathgrid", "", opcodeTogglePathgrid); extensions.registerInstruction ("tpg", "", opcodeTogglePathgrid); extensions.registerInstruction ("dontsaveobject", "", opcodeDontSaveObject); + extensions.registerInstruction ("pcforce1stperson", "", opcodePcForce1stPerson); + extensions.registerInstruction ("pcforce3rdperson", "", opcodePcForce3rdPerson); + extensions.registerFunction ("pcget3rdperson", 'l', "", opcodePcGet3rdPerson); extensions.registerInstruction ("togglevanitymode", "", opcodeToggleVanityMode); extensions.registerInstruction ("tvm", "", opcodeToggleVanityMode); extensions.registerFunction ("getpcsleep", 'l', "", opcodeGetPcSleep); @@ -272,20 +290,30 @@ namespace Compiler extensions.registerInstruction ("fall", "", opcodeFall, opcodeFallExplicit); extensions.registerFunction ("getstandingpc", 'l', "", opcodeGetStandingPc, opcodeGetStandingPcExplicit); extensions.registerFunction ("getstandingactor", 'l', "", opcodeGetStandingActor, opcodeGetStandingActorExplicit); + extensions.registerFunction ("getcollidingpc", 'l', "", opcodeGetCollidingPc, opcodeGetCollidingPcExplicit); + extensions.registerFunction ("getcollidingactor", 'l', "", opcodeGetCollidingActor, opcodeGetCollidingActorExplicit); + extensions.registerInstruction ("hurtstandingactor", "f", opcodeHurtStandingActor, opcodeHurtStandingActorExplicit); + extensions.registerInstruction ("hurtcollidingactor", "f", opcodeHurtCollidingActor, opcodeHurtCollidingActorExplicit); extensions.registerFunction ("getwindspeed", 'f', "", opcodeGetWindSpeed); extensions.registerFunction ("hitonme", 'l', "S", opcodeHitOnMe, opcodeHitOnMeExplicit); + extensions.registerFunction ("hitattemptonme", 'l', "S", opcodeHitAttemptOnMe, opcodeHitAttemptOnMeExplicit); extensions.registerInstruction ("disableteleporting", "", opcodeDisableTeleporting); extensions.registerInstruction ("enableteleporting", "", opcodeEnableTeleporting); extensions.registerInstruction ("showvars", "", opcodeShowVars, opcodeShowVarsExplicit); extensions.registerInstruction ("sv", "", opcodeShowVars, opcodeShowVarsExplicit); extensions.registerInstruction("tgm", "", opcodeToggleGodMode); extensions.registerInstruction("togglegodmode", "", opcodeToggleGodMode); + extensions.registerInstruction("togglescripts", "", opcodeToggleScripts); extensions.registerInstruction ("disablelevitation", "", opcodeDisableLevitation); extensions.registerInstruction ("enablelevitation", "", opcodeEnableLevitation); extensions.registerFunction ("getpcinjail", 'l', "", opcodeGetPcInJail); extensions.registerFunction ("getpctraveling", 'l', "", opcodeGetPcTraveling); extensions.registerInstruction ("betacomment", "S", opcodeBetaComment, opcodeBetaCommentExplicit); extensions.registerInstruction ("bc", "S", opcodeBetaComment, opcodeBetaCommentExplicit); + extensions.registerInstruction ("addtolevcreature", "ccl", opcodeAddToLevCreature); + extensions.registerInstruction ("removefromlevcreature", "ccl", opcodeRemoveFromLevCreature); + extensions.registerInstruction ("addtolevitem", "ccl", opcodeAddToLevItem); + extensions.registerInstruction ("removefromlevitem", "ccl", opcodeRemoveFromLevItem); } } @@ -354,6 +382,16 @@ namespace Compiler "mercantile", "speechcraft", "handtohand" }; + static const char *magicEffects[numberOfMagicEffects] = + { + "resistmagicka", "resistfire", "resistfrost", "resistshock", + "resistdisease", "resistblight", "resistcorprus", "resistpoison", + "resistparalysis", "resistnormalweapons", "waterbreathing", "chameleon", + "waterwalking", "swimspeed", "superjump", "flying", + "armorbonus", "castpenalty", "silence", "blindness", + "paralysis", "invisible", "attackbonus", "defendbonus" + }; + std::string get ("get"); std::string set ("set"); std::string mod ("mod"); @@ -402,11 +440,23 @@ namespace Compiler opcodeModSkill+i, opcodeModSkillExplicit+i); } + for (int i=0; i + #include #include "scanner.hpp" @@ -11,6 +13,7 @@ #include "generator.hpp" #include "extensions.hpp" #include "declarationparser.hpp" +#include "exception.hpp" namespace Compiler { @@ -54,7 +57,7 @@ namespace Compiler Literals& literals, std::vector& code, bool allowExpression) : Parser (errorHandler, context), mLocals (locals), mLiterals (literals), mCode (code), mState (BeginState), mExprParser (errorHandler, context, locals, literals), - mAllowExpression (allowExpression), mButtons(0), mType(0) + mAllowExpression (allowExpression), mButtons(0), mType(0), mReferenceMember(false) {} bool LineParser::parseInt (int value, const TokenLoc& loc, Scanner& scanner) @@ -119,7 +122,7 @@ namespace Compiler if (mState==SetMemberVarState) { - mMemberName = name; + mMemberName = Misc::StringUtils::lowerCase (name); std::pair type = getContext().getMemberType (mMemberName, mName); if (type.first!=' ') @@ -262,6 +265,20 @@ namespace Compiler Generator::disable (mCode, mLiterals, mExplicit); mState = PotentialEndState; return true; + + case Scanner::K_startscript: + + mExprParser.parseArguments ("c", scanner, mCode); + Generator::startScript (mCode, mLiterals, mExplicit); + mState = EndState; + return true; + + case Scanner::K_stopscript: + + mExprParser.parseArguments ("c", scanner, mCode); + Generator::stopScript (mCode); + mState = EndState; + return true; } // check for custom extensions @@ -278,9 +295,34 @@ namespace Compiler mExplicit.clear(); } - int optionals = mExprParser.parseArguments (argumentType, scanner, mCode); + try + { + // workaround for broken positioncell instructions. + /// \todo add option to disable this + std::auto_ptr errorDowngrade (0); + if (Misc::StringUtils::lowerCase (loc.mLiteral)=="positioncell") + errorDowngrade.reset (new ErrorDowngrade (getErrorHandler())); + + std::vector code; + int optionals = mExprParser.parseArguments (argumentType, scanner, code, keyword); + mCode.insert (mCode.end(), code.begin(), code.end()); + extensions->generateInstructionCode (keyword, mCode, mLiterals, + mExplicit, optionals); + } + catch (const SourceException&) + { + // Ignore argument exceptions for positioncell. + /// \todo add option to disable this + if (Misc::StringUtils::lowerCase (loc.mLiteral)=="positioncell") + { + SkipParser skip (getErrorHandler(), getContext()); + scanner.scan (skip); + return false; + } + + throw; + } - extensions->generateInstructionCode (keyword, mCode, mLiterals, mExplicit, optionals); mState = EndState; return true; } @@ -349,7 +391,7 @@ namespace Compiler if (declaration.parseKeyword (keyword, loc, scanner)) scanner.scan (declaration); - return true; + return false; } case Scanner::K_set: mState = SetState; return true; @@ -361,13 +403,6 @@ namespace Compiler mState = EndState; return true; - case Scanner::K_startscript: - - mExprParser.parseArguments ("c", scanner, mCode); - Generator::startScript (mCode); - mState = EndState; - return true; - case Scanner::K_stopscript: mExprParser.parseArguments ("c", scanner, mCode); @@ -454,6 +489,13 @@ namespace Compiler bool LineParser::parseSpecial (int code, const TokenLoc& loc, Scanner& scanner) { + if (mState==EndState && code==Scanner::S_open) + { + getErrorHandler().warning ("stray '[' or '(' at the end of the line (ignoring it)", + loc); + return true; + } + if (code==Scanner::S_newline && (mState==EndState || mState==BeginState || mState==PotentialEndState)) return false; diff --git a/components/compiler/locals.hpp b/components/compiler/locals.hpp index d5bf05253..1b2ae6042 100644 --- a/components/compiler/locals.hpp +++ b/components/compiler/locals.hpp @@ -15,10 +15,6 @@ namespace Compiler std::vector mLongs; std::vector mFloats; - int searchIndex (char type, const std::string& name) const; - - bool search (char type, const std::string& name) const; - std::vector& get (char type); public: @@ -29,6 +25,12 @@ namespace Compiler int getIndex (const std::string& name) const; ///< return index for local variable \a name (-1: does not exist). + bool search (char type, const std::string& name) const; + + /// Return index for local variable \a name of type \a type (-1: variable does not + /// exit). + int searchIndex (char type, const std::string& name) const; + const std::vector& get (char type) const; void write (std::ostream& localFile) const; diff --git a/components/compiler/opcodes.hpp b/components/compiler/opcodes.hpp index 9f3ed3574..a4aab8aa1 100644 --- a/components/compiler/opcodes.hpp +++ b/components/compiler/opcodes.hpp @@ -123,6 +123,14 @@ namespace Compiler const int opcodeClearForceRunExplicit = 0x2000155; const int opcodeForceRun = 0x2000156; const int opcodeForceRunExplicit = 0x2000157; + const int opcodeClearForceJump = 0x2000258; + const int opcodeClearForceJumpExplicit = 0x2000259; + const int opcodeForceJump = 0x200025a; + const int opcodeForceJumpExplicit = 0x200025b; + const int opcodeClearForceMoveJump = 0x200025c; + const int opcodeClearForceMoveJumpExplicit = 0x200025d; + const int opcodeForceMoveJump = 0x200025e; + const int opcodeForceMoveJumpExplicit = 0x200025f; const int opcodeClearForceSneak = 0x2000158; const int opcodeClearForceSneakExplicit = 0x2000159; const int opcodeForceSneak = 0x200015a; @@ -134,6 +142,10 @@ namespace Compiler const int opcodeGetForceSneak = 0x20001cc; const int opcodeGetForceRunExplicit = 0x20001cd; const int opcodeGetForceSneakExplicit = 0x20001ce; + const int opcodeGetForceJump = 0x2000260; + const int opcodeGetForceMoveJump = 0x2000262; + const int opcodeGetForceJumpExplicit = 0x2000261; + const int opcodeGetForceMoveJumpExplicit = 0x2000263; } namespace Dialogue @@ -155,6 +167,7 @@ namespace Compiler const int opcodeSameFaction = 0x20001b5; const int opcodeSameFactionExplicit = 0x20001b6; const int opcodeModFactionReaction = 0x2000242; + const int opcodeSetFactionReaction = 0x20002ff; const int opcodeGetFactionReaction = 0x2000243; const int opcodeClearInfoActor = 0x2000245; const int opcodeClearInfoActorExplicit = 0x2000246; @@ -172,6 +185,7 @@ namespace Compiler const int opcodeEnableMapMenu = 0x2000015; const int opcodeEnableStatsMenu = 0x2000016; const int opcodeEnableRest = 0x2000017; + const int opcodeEnableLevelupMenu = 0x2000300; const int opcodeShowRestMenu = 0x2000018; const int opcodeShowRestMenuExplicit = 0x2000234; const int opcodeGetButtonPressed = 0x2000137; @@ -200,8 +214,12 @@ namespace Compiler const int opcodeFadeOut = 0x200013d; const int opcodeFadeTo = 0x200013e; const int opcodeToggleWater = 0x2000144; + const int opcodeToggleWorld = 0x20002f5; const int opcodeTogglePathgrid = 0x2000146; const int opcodeDontSaveObject = 0x2000153; + const int opcodePcForce1stPerson = 0x20002f6; + const int opcodePcForce3rdPerson = 0x20002f7; + const int opcodePcGet3rdPerson = 0x20002f8; const int opcodeToggleVanityMode = 0x2000174; const int opcodeGetPcSleep = 0x200019f; const int opcodeGetPcJumping = 0x2000233; @@ -238,6 +256,14 @@ namespace Compiler const int opcodeGetStandingPcExplicit = 0x200020d; const int opcodeGetStandingActor = 0x200020e; const int opcodeGetStandingActorExplicit = 0x200020f; + const int opcodeGetCollidingPc = 0x2000250; + const int opcodeGetCollidingPcExplicit = 0x2000251; + const int opcodeGetCollidingActor = 0x2000252; + const int opcodeGetCollidingActorExplicit = 0x2000253; + const int opcodeHurtStandingActor = 0x2000254; + const int opcodeHurtStandingActorExplicit = 0x2000255; + const int opcodeHurtCollidingActor = 0x2000256; + const int opcodeHurtCollidingActorExplicit = 0x2000257; const int opcodeGetWindSpeed = 0x2000212; const int opcodePlayBink = 0x20001f7; const int opcodeGoToJail = 0x2000235; @@ -245,11 +271,14 @@ namespace Compiler const int opcodePayFineThief = 0x2000237; const int opcodeHitOnMe = 0x2000213; const int opcodeHitOnMeExplicit = 0x2000214; + const int opcodeHitAttemptOnMe = 0x20002f9; + const int opcodeHitAttemptOnMeExplicit = 0x20002fa; const int opcodeDisableTeleporting = 0x2000215; const int opcodeEnableTeleporting = 0x2000216; const int opcodeShowVars = 0x200021d; const int opcodeShowVarsExplicit = 0x200021e; const int opcodeToggleGodMode = 0x200021f; + const int opcodeToggleScripts = 0x2000301; const int opcodeDisableLevitation = 0x2000220; const int opcodeEnableLevitation = 0x2000221; const int opcodeCast = 0x2000227; @@ -258,6 +287,10 @@ namespace Compiler const int opcodeExplodeSpellExplicit = 0x200022a; const int opcodeGetPcInJail = 0x200023e; const int opcodeGetPcTraveling = 0x200023f; + const int opcodeAddToLevCreature = 0x20002fb; + const int opcodeRemoveFromLevCreature = 0x20002fc; + const int opcodeAddToLevItem = 0x20002fd; + const int opcodeRemoveFromLevItem = 0x20002fe; } namespace Sky @@ -302,6 +335,8 @@ namespace Compiler const int numberOfDynamics = 3; const int numberOfSkills = 27; + const int numberOfMagicEffects = 24; + const int opcodeGetAttribute = 0x2000027; const int opcodeGetAttributeExplicit = 0x200002f; const int opcodeSetAttribute = 0x2000037; @@ -327,6 +362,13 @@ namespace Compiler const int opcodeModSkill = 0x20000fa; const int opcodeModSkillExplicit = 0x2000115; + const int opcodeGetMagicEffect = 0x2000264; + const int opcodeGetMagicEffectExplicit = 0x200027c; + const int opcodeSetMagicEffect = 0x2000294; + const int opcodeSetMagicEffectExplicit = 0x20002ac; + const int opcodeModMagicEffect = 0x20002c4; + const int opcodeModMagicEffectExplicit = 0x20002dc; + const int opcodeGetPCCrimeLevel = 0x20001ec; const int opcodeSetPCCrimeLevel = 0x20001ed; const int opcodeModPCCrimeLevel = 0x20001ee; @@ -455,6 +497,7 @@ namespace Compiler const int opcodeMoveExplicit = 0x2000207; const int opcodeMoveWorld = 0x2000208; const int opcodeMoveWorldExplicit = 0x2000209; + const int opcodeResetActors = 0x20002f4; } namespace User diff --git a/components/compiler/parser.cpp b/components/compiler/parser.cpp index 781fbad8c..0f442c350 100644 --- a/components/compiler/parser.cpp +++ b/components/compiler/parser.cpp @@ -21,13 +21,6 @@ namespace Compiler throw SourceException(); } - // Report the error - - void Parser::reportError (const std::string& message, const TokenLoc& loc) - { - mErrorHandler.error (message, loc); - } - // Report the warning without throwing an exception. void Parser::reportWarning (const std::string& message, const TokenLoc& loc) diff --git a/components/compiler/parser.hpp b/components/compiler/parser.hpp index 54e913b20..2ef6e85b9 100644 --- a/components/compiler/parser.hpp +++ b/components/compiler/parser.hpp @@ -26,9 +26,6 @@ namespace Compiler void reportSeriousError (const std::string& message, const TokenLoc& loc); ///< Report the error and throw a exception. - void reportError (const std::string& message, const TokenLoc& loc); - ///< Report the error - void reportWarning (const std::string& message, const TokenLoc& loc); ///< Report the warning without throwing an exception. diff --git a/components/compiler/scanner.cpp b/components/compiler/scanner.cpp index 46e50a2e9..83d435962 100644 --- a/components/compiler/scanner.cpp +++ b/components/compiler/scanner.cpp @@ -48,6 +48,9 @@ namespace Compiler bool Scanner::scanToken (Parser& parser) { + bool allowDigit = mNameStartingWithDigit; + mNameStartingWithDigit = false; + switch (mPutback) { case Putback_Special: @@ -112,6 +115,7 @@ namespace Compiler else if (isWhitespace (c)) { mLoc.mLiteral.clear(); + mNameStartingWithDigit = allowDigit; return true; } else if (c==':') @@ -120,21 +124,21 @@ namespace Compiler mLoc.mLiteral.clear(); return true; } - else if (std::isdigit (c)) + else if (std::isalpha (c) || c=='_' || c=='"' || (allowDigit && std::isdigit (c))) { bool cont = false; - if (scanInt (c, parser, cont)) + if (scanName (c, parser, cont)) { mLoc.mLiteral.clear(); return cont; } } - else if (std::isalpha (c) || c=='_' || c=='"') + else if (std::isdigit (c)) { bool cont = false; - if (scanName (c, parser, cont)) + if (scanInt (c, parser, cont)) { mLoc.mLiteral.clear(); return cont; @@ -266,8 +270,9 @@ namespace Compiler bool Scanner::scanName (char c, Parser& parser, bool& cont) { std::string name; + name += c; - if (!scanName (c, name)) + if (!scanName (name)) return false; TokenLoc loc (mLoc); @@ -308,15 +313,11 @@ namespace Compiler return true; } - bool Scanner::scanName (char c, std::string& name) + bool Scanner::scanName (std::string& name) { - bool first = false; + char c; bool error = false; - name.clear(); - - putback (c); - while (get (c)) { if (!name.empty() && name[0]=='"') @@ -331,34 +332,28 @@ namespace Compiler // { // if (!get (c)) // { +// error = true; // mErrorHandler.error ("incomplete escape sequence", mLoc); // break; // } // } else if (c=='\n') { + error = true; mErrorHandler.error ("incomplete string or name", mLoc); break; } } else if (!(c=='"' && name.empty())) { - if (!(std::isalpha (c) || std::isdigit (c) || c=='_' || c=='`' || - /// \todo add an option to disable the following hack. Also, find out who is - /// responsible for allowing it in the first place and meet up with that person in - /// a dark alley. - (c=='-' && !name.empty() && std::isalpha (mStream.peek())))) + if (!isStringCharacter (c)) { putback (c); break; } - - if (first && std::isdigit (c)) - error = true; } name += c; - first = false; } return !error; @@ -391,14 +386,17 @@ namespace Compiler { if (get (c)) { - if (c=='=') + /// \todo hack to allow a space in comparison operators (add option to disable) + if (c==' ' && !get (c)) + special = S_cmpEQ; + else if (c=='=') special = S_cmpEQ; else { special = S_cmpEQ; putback (c); // return false; -// Allow = as synonym for ==. \todo optionally disable for post-1.0 scripting improvements. +/// Allow = as synonym for ==. \todo optionally disable for post-1.0 scripting improvements. } } else @@ -411,6 +409,10 @@ namespace Compiler { if (get (c)) { + /// \todo hack to allow a space in comparison operators (add option to disable) + if (c==' ' && !get (c)) + return false; + if (c=='=') special = S_cmpNE; else @@ -437,11 +439,40 @@ namespace Compiler else special = S_minus; } + else if (static_cast (c)==0xe2) + { + /// Workaround for some translator who apparently can't keep his minus in order + /// \todo disable for later script formats + if (get (c) && static_cast (c)==0x80 && + get (c) && static_cast (c)==0x93) + { + if (get (c)) + { + if (c=='>') + special = S_ref; + else + { + putback (c); + special = S_minus; + } + } + else + special = S_minus; + } + else + { + mErrorHandler.error ("Invalid character", mLoc); + return false; + } + } else if (c=='<') { if (get (c)) { - if (c=='=') + /// \todo hack to allow a space in comparison operators (add option to disable) + if (c==' ' && !get (c)) + special = S_cmpLT; + else if (c=='=') { special = S_cmpLE; @@ -461,7 +492,10 @@ namespace Compiler { if (get (c)) { - if (c=='=') + /// \todo hack to allow a space in comparison operators (add option to disable) + if (c==' ' && !get (c)) + special = S_cmpGT; + else if (c=='=') { special = S_cmpGE; @@ -499,6 +533,17 @@ namespace Compiler return true; } + bool Scanner::isStringCharacter (char c, bool lookAhead) + { + return std::isalpha (c) || std::isdigit (c) || c=='_' || + /// \todo disable this when doing more stricter compiling + c=='`' || c=='\'' || + /// \todo disable this when doing more stricter compiling. Also, find out who is + /// responsible for allowing it in the first place and meet up with that person in + /// a dark alley. + (c=='-' && (!lookAhead || isStringCharacter (mStream.peek(), false))); + } + bool Scanner::isWhitespace (char c) { return c==' ' || c=='\t'; @@ -509,7 +554,8 @@ namespace Compiler Scanner::Scanner (ErrorHandler& errorHandler, std::istream& inputStream, const Extensions *extensions) : mErrorHandler (errorHandler), mStream (inputStream), mExtensions (extensions), - mPutback (Putback_None), mPutbackCode(0), mPutbackInteger(0), mPutbackFloat(0) + mPutback (Putback_None), mPutbackCode(0), mPutbackInteger(0), mPutbackFloat(0), + mNameStartingWithDigit (false) { } @@ -561,4 +607,9 @@ namespace Compiler if (mExtensions) mExtensions->listKeywords (keywords); } + + void Scanner::allowNameStartingwithDigit() + { + mNameStartingWithDigit = true; + } } diff --git a/components/compiler/scanner.hpp b/components/compiler/scanner.hpp index 344ae0582..ed01bbe44 100644 --- a/components/compiler/scanner.hpp +++ b/components/compiler/scanner.hpp @@ -37,6 +37,7 @@ namespace Compiler float mPutbackFloat; std::string mPutbackName; TokenLoc mPutbackLoc; + bool mNameStartingWithDigit; public: @@ -88,10 +89,13 @@ namespace Compiler bool scanName (char c, Parser& parser, bool& cont); - bool scanName (char c, std::string& name); + /// \param name May contain the start of the name (one or more characters) + bool scanName (std::string& name); bool scanSpecial (char c, Parser& parser, bool& cont); + bool isStringCharacter (char c, bool lookAhead = true); + static bool isWhitespace (char c); public: @@ -120,6 +124,9 @@ namespace Compiler void listKeywords (std::vector& keywords); ///< Append all known keywords to \a kaywords. + + /// For the next token allow names to start with a digit. + void allowNameStartingwithDigit(); }; } diff --git a/components/compiler/streamerrorhandler.cpp b/components/compiler/streamerrorhandler.cpp index 8a74ad086..fc1a05943 100644 --- a/components/compiler/streamerrorhandler.cpp +++ b/components/compiler/streamerrorhandler.cpp @@ -16,7 +16,7 @@ namespace Compiler mStream << "warning "; mStream - << "line " << loc.mLine << ", column " << loc.mColumn + << "line " << loc.mLine+1 << ", column " << loc.mColumn+1 << " (" << loc.mLiteral << ")" << std::endl << " " << message << std::endl; } diff --git a/apps/launcher/settings/gamesettings.cpp b/components/config/gamesettings.cpp similarity index 78% rename from apps/launcher/settings/gamesettings.cpp rename to components/config/gamesettings.cpp index 2f3dd9a45..0481235c7 100644 --- a/apps/launcher/settings/gamesettings.cpp +++ b/components/config/gamesettings.cpp @@ -1,4 +1,5 @@ #include "gamesettings.hpp" +#include "launchersettings.hpp" #include #include @@ -26,21 +27,19 @@ namespace boost } /* namespace boost */ #endif /* (BOOST_VERSION <= 104600) */ +const char Config::GameSettings::sContentKey[] = "content"; -Launcher::GameSettings::GameSettings(Files::ConfigurationManager &cfg) +Config::GameSettings::GameSettings(Files::ConfigurationManager &cfg) : mCfgMgr(cfg) { } -Launcher::GameSettings::~GameSettings() +Config::GameSettings::~GameSettings() { } -void Launcher::GameSettings::validatePaths() +void Config::GameSettings::validatePaths() { - if (mSettings.isEmpty() || !mDataDirs.isEmpty()) - return; // Don't re-validate paths if they are already parsed - QStringList paths = mSettings.values(QString("data")); Files::PathContainer dataDirs; @@ -84,24 +83,24 @@ void Launcher::GameSettings::validatePaths() } } -QStringList Launcher::GameSettings::values(const QString &key, const QStringList &defaultValues) +QStringList Config::GameSettings::values(const QString &key, const QStringList &defaultValues) const { if (!mSettings.values(key).isEmpty()) return mSettings.values(key); return defaultValues; } -bool Launcher::GameSettings::readFile(QTextStream &stream) +bool Config::GameSettings::readFile(QTextStream &stream) { return readFile(stream, mSettings); } -bool Launcher::GameSettings::readUserFile(QTextStream &stream) +bool Config::GameSettings::readUserFile(QTextStream &stream) { return readFile(stream, mUserSettings); } -bool Launcher::GameSettings::readFile(QTextStream &stream, QMap &settings) +bool Config::GameSettings::readFile(QTextStream &stream, QMap &settings) { QMap cache; QRegExp keyRe("^([^=]+)\\s*=\\s*(.+)$"); @@ -143,8 +142,7 @@ bool Launcher::GameSettings::readFile(QTextStream &stream, QMap i(mUserSettings); @@ -153,9 +151,6 @@ bool Launcher::GameSettings::writeFile(QTextStream &stream) while (i.hasPrevious()) { i.previous(); - if (i.key() == QLatin1String("content")) - continue; - // Quote paths with spaces if (i.key() == QLatin1String("data") || i.key() == QLatin1String("data-local") @@ -175,18 +170,13 @@ bool Launcher::GameSettings::writeFile(QTextStream &stream) } - QStringList content = mUserSettings.values(QString("content")); - for (int i = content.count(); i--;) { - stream << "content=" << content.at(i) << "\n"; - } - return true; } -bool Launcher::GameSettings::hasMaster() +bool Config::GameSettings::hasMaster() { bool result = false; - QStringList content = mSettings.values(QString("content")); + QStringList content = mSettings.values(QString(Config::GameSettings::sContentKey)); for (int i = 0; i < content.count(); ++i) { if (content.at(i).contains(".omwgame") || content.at(i).contains(".esm")) { result = true; @@ -196,3 +186,19 @@ bool Launcher::GameSettings::hasMaster() return result; } + +void Config::GameSettings::setContentList(const QStringList& fileNames) +{ + remove(sContentKey); + foreach(const QString& fileName, fileNames) + { + setMultiValue(sContentKey, fileName); + } +} + +QStringList Config::GameSettings::getContentList() const +{ + // QMap returns multiple rows in LIFO order, so need to reverse + return Config::LauncherSettings::reverse(values(sContentKey)); +} + diff --git a/apps/launcher/settings/gamesettings.hpp b/components/config/gamesettings.hpp similarity index 87% rename from apps/launcher/settings/gamesettings.hpp rename to components/config/gamesettings.hpp index df8215074..cc5033f35 100644 --- a/apps/launcher/settings/gamesettings.hpp +++ b/components/config/gamesettings.hpp @@ -14,7 +14,7 @@ namespace Files struct ConfigurationManager; } -namespace Launcher +namespace Config { class GameSettings { @@ -52,12 +52,14 @@ namespace Launcher } inline QStringList getDataDirs() { return mDataDirs; } + + inline void removeDataDir(const QString &dir) { if(!dir.isEmpty()) mDataDirs.removeAll(dir); } inline void addDataDir(const QString &dir) { if(!dir.isEmpty()) mDataDirs.append(dir); } inline QString getDataLocal() {return mDataLocal; } bool hasMaster(); - QStringList values(const QString &key, const QStringList &defaultValues = QStringList()); + QStringList values(const QString &key, const QStringList &defaultValues = QStringList()) const; bool readFile(QTextStream &stream); bool readFile(QTextStream &stream, QMap &settings); @@ -65,6 +67,9 @@ namespace Launcher bool writeFile(QTextStream &stream); + void setContentList(const QStringList& fileNames); + QStringList getContentList() const; + private: Files::ConfigurationManager &mCfgMgr; @@ -74,6 +79,8 @@ namespace Launcher QStringList mDataDirs; QString mDataLocal; + + static const char sContentKey[]; }; } #endif // GAMESETTINGS_HPP diff --git a/components/config/launchersettings.cpp b/components/config/launchersettings.cpp new file mode 100644 index 000000000..1d4b428c9 --- /dev/null +++ b/components/config/launchersettings.cpp @@ -0,0 +1,204 @@ +#include "launchersettings.hpp" + +#include +#include +#include +#include + +#include + +const char Config::LauncherSettings::sCurrentContentListKey[] = "Profiles/currentprofile"; +const char Config::LauncherSettings::sLauncherConfigFileName[] = "launcher.cfg"; +const char Config::LauncherSettings::sContentListsSectionPrefix[] = "Profiles/"; +const char Config::LauncherSettings::sContentListSuffix[] = "/content"; + +Config::LauncherSettings::LauncherSettings() +{ +} + +Config::LauncherSettings::~LauncherSettings() +{ +} + +QStringList Config::LauncherSettings::subKeys(const QString &key) +{ + QMap settings = SettingsBase::getSettings(); + QStringList keys = settings.uniqueKeys(); + + qDebug() << keys; + + QRegExp keyRe("(.+)/"); + + QStringList result; + + foreach (const QString ¤tKey, keys) { + + if (keyRe.indexIn(currentKey) != -1) + { + QString prefixedKey = keyRe.cap(1); + + if(prefixedKey.startsWith(key)) + { + QString subKey = prefixedKey.remove(key); + if (!subKey.isEmpty()) + result.append(subKey); + } + } + } + + result.removeDuplicates(); + return result; +} + + +bool Config::LauncherSettings::writeFile(QTextStream &stream) +{ + QString sectionPrefix; + QRegExp sectionRe("([^/]+)/(.+)$"); + QMap settings = SettingsBase::getSettings(); + + QMapIterator i(settings); + i.toBack(); + + while (i.hasPrevious()) { + i.previous(); + + QString prefix; + QString key; + + if (sectionRe.exactMatch(i.key())) { + prefix = sectionRe.cap(1); + key = sectionRe.cap(2); + } + + // Get rid of legacy settings + if (key.contains(QChar('\\'))) + continue; + + if (key == QLatin1String("CurrentProfile")) + continue; + + if (sectionPrefix != prefix) { + sectionPrefix = prefix; + stream << "\n[" << prefix << "]\n"; + } + + stream << key << "=" << i.value() << "\n"; + } + + return true; + +} + +QStringList Config::LauncherSettings::getContentLists() +{ + return subKeys(QString(sContentListsSectionPrefix)); +} + +QString Config::LauncherSettings::makeContentListKey(const QString& contentListName) +{ + return QString(sContentListsSectionPrefix) + contentListName + QString(sContentListSuffix); +} + +void Config::LauncherSettings::setContentList(const GameSettings& gameSettings) +{ + // obtain content list from game settings (if present) + const QStringList files(gameSettings.getContentList()); + + // if openmw.cfg has no content, exit so we don't create an empty content list. + if (files.isEmpty()) + { + return; + } + + // if any existing profile in launcher matches the content list, make that profile the default + foreach(const QString &listName, getContentLists()) + { + if (isEqual(files, getContentListFiles(listName))) + { + setCurrentContentListName(listName); + return; + } + } + + // otherwise, add content list + QString newContentListName(makeNewContentListName()); + setCurrentContentListName(newContentListName); + setContentList(newContentListName, files); +} + +void Config::LauncherSettings::removeContentList(const QString &contentListName) +{ + remove(makeContentListKey(contentListName)); +} + +void Config::LauncherSettings::setCurrentContentListName(const QString &contentListName) +{ + remove(QString(sCurrentContentListKey)); + setValue(QString(sCurrentContentListKey), contentListName); +} + +void Config::LauncherSettings::setContentList(const QString& contentListName, const QStringList& fileNames) +{ + removeContentList(contentListName); + QString key = makeContentListKey(contentListName); + foreach(const QString& fileName, fileNames) + { + setMultiValue(key, fileName); + } +} + +QString Config::LauncherSettings::getCurrentContentListName() const +{ + return value(QString(sCurrentContentListKey)); +} + +QStringList Config::LauncherSettings::getContentListFiles(const QString& contentListName) const +{ + // QMap returns multiple rows in LIFO order, so need to reverse + return reverse(getSettings().values(makeContentListKey(contentListName))); +} + +QStringList Config::LauncherSettings::reverse(const QStringList& toReverse) +{ + QStringList result; + result.reserve(toReverse.size()); + std::reverse_copy(toReverse.begin(), toReverse.end(), std::back_inserter(result)); + return result; +} + +bool Config::LauncherSettings::isEqual(const QStringList& list1, const QStringList& list2) +{ + if (list1.count() != list2.count()) + { + return false; + } + + for (int i = 0; i < list1.count(); ++i) + { + if (list1.at(i) != list2.at(i)) + { + return false; + } + } + + // if get here, lists are same + return true; +} + +QString Config::LauncherSettings::makeNewContentListName() +{ + // basically, use date and time as the name e.g. YYYY-MM-DDThh:mm:ss + time_t rawtime; + struct tm * timeinfo; + + time(&rawtime); + timeinfo = localtime(&rawtime); + int base = 10; + QChar zeroPad('0'); + return QString("%1-%2-%3T%4:%5:%6") + .arg(timeinfo->tm_year + 1900, 4).arg(timeinfo->tm_mon + 1, 2, base, zeroPad).arg(timeinfo->tm_mday, 2, base, zeroPad) + .arg(timeinfo->tm_hour, 2, base, zeroPad).arg(timeinfo->tm_min, 2, base, zeroPad).arg(timeinfo->tm_sec, 2, base, zeroPad); +} + + diff --git a/components/config/launchersettings.hpp b/components/config/launchersettings.hpp new file mode 100644 index 000000000..cbe21c54a --- /dev/null +++ b/components/config/launchersettings.hpp @@ -0,0 +1,60 @@ +#ifndef LAUNCHERSETTINGS_HPP +#define LAUNCHERSETTINGS_HPP + +#include "settingsbase.hpp" +#include "gamesettings.hpp" + +namespace Config +{ + class LauncherSettings : public SettingsBase > + { + public: + LauncherSettings(); + ~LauncherSettings(); + + bool writeFile(QTextStream &stream); + + /// \return names of all Content Lists in the launcher's .cfg file. + QStringList getContentLists(); + + /// Set initally selected content list to match values from openmw.cfg, creating if necessary + void setContentList(const GameSettings& gameSettings); + + /// Create a Content List (or replace if it already exists) + void setContentList(const QString& contentListName, const QStringList& fileNames); + + void removeContentList(const QString &contentListName); + + void setCurrentContentListName(const QString &contentListName); + + QString getCurrentContentListName() const; + + QStringList getContentListFiles(const QString& contentListName) const; + + /// \return new list that is reversed order of input + static QStringList reverse(const QStringList& toReverse); + + static const char sLauncherConfigFileName[]; + + private: + + /// \return key to use to get/set the files in the specified Content List + static QString makeContentListKey(const QString& contentListName); + + /// \return true if both lists are same + static bool isEqual(const QStringList& list1, const QStringList& list2); + + static QString makeNewContentListName(); + + QStringList subKeys(const QString &key); + + /// name of entry in launcher.cfg that holds name of currently selected Content List + static const char sCurrentContentListKey[]; + + /// section of launcher.cfg holding the Content Lists + static const char sContentListsSectionPrefix[]; + + static const char sContentListSuffix[]; + }; +} +#endif // LAUNCHERSETTINGS_HPP diff --git a/apps/launcher/settings/settingsbase.hpp b/components/config/settingsbase.hpp similarity index 85% rename from apps/launcher/settings/settingsbase.hpp rename to components/config/settingsbase.hpp index 3a1cf8e30..c798d2893 100644 --- a/apps/launcher/settings/settingsbase.hpp +++ b/components/config/settingsbase.hpp @@ -7,7 +7,7 @@ #include #include -namespace Launcher +namespace Config { template class SettingsBase @@ -17,7 +17,7 @@ namespace Launcher SettingsBase() { mMultiValue = false; } ~SettingsBase() {} - inline QString value(const QString &key, const QString &defaultValue = QString()) + inline QString value(const QString &key, const QString &defaultValue = QString()) const { return mSettings.value(key).isEmpty() ? defaultValue : mSettings.value(key); } @@ -46,11 +46,11 @@ namespace Launcher mSettings.remove(key); } - Map getSettings() {return mSettings;} + Map getSettings() const {return mSettings;} bool readFile(QTextStream &stream) { - mCache.clear(); + Map cache; QString sectionPrefix; @@ -79,31 +79,30 @@ namespace Launcher mSettings.remove(key); - QStringList values = mCache.values(key); + QStringList values = cache.values(key); if (!values.contains(value)) { if (mMultiValue) { - mCache.insertMulti(key, value); + cache.insertMulti(key, value); } else { - mCache.insert(key, value); + cache.insert(key, value); } } } } if (mSettings.isEmpty()) { - mSettings = mCache; // This is the first time we read a file + mSettings = cache; // This is the first time we read a file return true; } // Merge the changed keys with those which didn't - mSettings.unite(mCache); + mSettings.unite(cache); return true; } private: Map mSettings; - Map mCache; bool mMultiValue; }; diff --git a/components/contentselector/model/contentmodel.cpp b/components/contentselector/model/contentmodel.cpp index e76930e81..3dc02af21 100644 --- a/components/contentselector/model/contentmodel.cpp +++ b/components/contentselector/model/contentmodel.cpp @@ -9,18 +9,24 @@ #include "components/esm/esmreader.hpp" -ContentSelectorModel::ContentModel::ContentModel(QObject *parent) : +ContentSelectorModel::ContentModel::ContentModel(QObject *parent, QIcon warningIcon) : QAbstractTableModel(parent), + mWarningIcon(warningIcon), mMimeType ("application/omwcontent"), mMimeTypes (QStringList() << mMimeType), mColumnCount (1), - mDragDropFlags (Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled), - mDropActions (Qt::CopyAction | Qt::MoveAction) + mDropActions (Qt::MoveAction) { setEncoding ("win1252"); uncheckAll(); } +ContentSelectorModel::ContentModel::~ContentModel() +{ + qDeleteAll(mFiles); + mFiles.clear(); +} + void ContentSelectorModel::ContentModel::setEncoding(const QString &encoding) { mEncoding = encoding; @@ -77,7 +83,7 @@ const ContentSelectorModel::EsmFile *ContentSelectorModel::ContentModel::item(co foreach (const EsmFile *file, mFiles) { - if (name == file->fileProperty (fp).toString()) + if (name.compare(file->fileProperty (fp).toString(), Qt::CaseInsensitive) == 0) return file; } return 0; @@ -97,7 +103,7 @@ QModelIndex ContentSelectorModel::ContentModel::indexFromItem(const EsmFile *ite Qt::ItemFlags ContentSelectorModel::ContentModel::flags(const QModelIndex &index) const { if (!index.isValid()) - return Qt::NoItemFlags; + return Qt::ItemIsDropEnabled; const EsmFile *file = item(index.row()); @@ -106,13 +112,13 @@ Qt::ItemFlags ContentSelectorModel::ContentModel::flags(const QModelIndex &index //game files can always be checked if (file->isGameFile()) - return Qt::ItemIsEnabled | Qt::ItemIsSelectable; + return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable; Qt::ItemFlags returnFlags; - bool allDependenciesFound = true; - bool gamefileChecked = false; - //addon can be checked if its gamefile is and all other dependencies exist + // addon can be checked if its gamefile is + // ... special case, addon with no dependency can be used with any gamefile. + bool gamefileChecked = (file->gameFiles().count() == 0); foreach (const QString &fileName, file->gameFiles()) { bool depFound = false; @@ -120,7 +126,7 @@ Qt::ItemFlags ContentSelectorModel::ContentModel::flags(const QModelIndex &index { //compare filenames only. Multiple instances //of the filename (with different paths) is not relevant here. - depFound = (dependency->fileName() == fileName); + depFound = (dependency->fileName().compare(fileName, Qt::CaseInsensitive) == 0); if (!depFound) continue; @@ -138,16 +144,11 @@ Qt::ItemFlags ContentSelectorModel::ContentModel::flags(const QModelIndex &index if (gamefileChecked || !(dependency->isGameFile())) break; } - - allDependenciesFound = allDependenciesFound && depFound; } if (gamefileChecked) { - if (allDependenciesFound) - returnFlags = returnFlags | Qt::ItemIsEnabled | Qt::ItemIsSelectable | mDragDropFlags; - else - returnFlags = Qt::ItemIsSelectable; + returnFlags = Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable | Qt::ItemIsDragEnabled; } return returnFlags; @@ -170,6 +171,11 @@ QVariant ContentSelectorModel::ContentModel::data(const QModelIndex &index, int switch (role) { + case Qt::DecorationRole: + { + return isLoadOrderError(file) ? mWarningIcon : QVariant(); + } + case Qt::EditRole: case Qt::DisplayRole: { @@ -177,7 +183,6 @@ QVariant ContentSelectorModel::ContentModel::data(const QModelIndex &index, int return file->fileProperty(static_cast(column)); return QVariant(); - break; } case Qt::TextAlignmentRole: @@ -193,8 +198,6 @@ QVariant ContentSelectorModel::ContentModel::data(const QModelIndex &index, int default: return Qt::AlignLeft + Qt::AlignVCenter; } - return QVariant(); - break; } case Qt::ToolTipRole: @@ -202,8 +205,7 @@ QVariant ContentSelectorModel::ContentModel::data(const QModelIndex &index, int if (column != 0) return QVariant(); - return file->toolTip(); - break; + return toolTip(file); } case Qt::CheckStateRole: @@ -212,8 +214,6 @@ QVariant ContentSelectorModel::ContentModel::data(const QModelIndex &index, int return QVariant(); return mCheckStates[file->filePath()]; - - break; } case Qt::UserRole: @@ -229,7 +229,6 @@ QVariant ContentSelectorModel::ContentModel::data(const QModelIndex &index, int case Qt::UserRole + 1: return isChecked(file->filePath()); - break; } return QVariant(); } @@ -276,7 +275,6 @@ bool ContentSelectorModel::ContentModel::setData(const QModelIndex &index, const case Qt::CheckStateRole: { int checkValue = value.toInt(); - bool success = false; bool setState = false; if ((checkValue==Qt::Checked) && !isChecked(file->filePath())) { @@ -292,7 +290,7 @@ bool ContentSelectorModel::ContentModel::setData(const QModelIndex &index, const { setCheckState(file->filePath(), success); emit dataChanged(index, index); - + checkForLoadOrderErrors(); } else return success; @@ -300,7 +298,7 @@ bool ContentSelectorModel::ContentModel::setData(const QModelIndex &index, const foreach (EsmFile *file, mFiles) { - if (file->gameFiles().contains(fileName)) + if (file->gameFiles().contains(fileName, Qt::CaseInsensitive)) { QModelIndex idx = indexFromItem(file); emit dataChanged(idx, idx); @@ -342,6 +340,8 @@ bool ContentSelectorModel::ContentModel::removeRows(int position, int rows, cons } endRemoveRows(); + // at this point we know that drag and drop has finished. + checkForLoadOrderErrors(); return true; } @@ -437,15 +437,12 @@ void ContentSelectorModel::ContentModel::addFiles(const QString &path) filters << "*.esp" << "*.esm" << "*.omwgame" << "*.omwaddon"; dir.setNameFilters(filters); - QTextCodec *codec = QTextCodec::codecForName("UTF8"); - - // Create a decoder for non-latin characters in esx metadata - QTextDecoder *decoder = codec->makeDecoder(); - foreach (const QString &path, dir.entryList()) { QFileInfo info(dir.absoluteFilePath(path)); - EsmFile *file = new EsmFile(path); + + if (item(info.absoluteFilePath()) != 0) + continue; try { ESM::ESMReader fileReader; @@ -454,19 +451,27 @@ void ContentSelectorModel::ContentModel::addFiles(const QString &path) fileReader.setEncoder(&encoder); fileReader.open(dir.absoluteFilePath(path).toStdString()); + EsmFile *file = new EsmFile(path); + foreach (const ESM::Header::MasterData &item, fileReader.getGameFiles()) - file->addGameFile(QString::fromStdString(item.name)); + file->addGameFile(QString::fromUtf8(item.name.c_str())); - file->setAuthor (decoder->toUnicode(fileReader.getAuthor().c_str())); + file->setAuthor (QString::fromUtf8(fileReader.getAuthor().c_str())); file->setDate (info.lastModified()); file->setFormat (fileReader.getFormat()); file->setFilePath (info.absoluteFilePath()); - file->setDescription(decoder->toUnicode(fileReader.getDesc().c_str())); + file->setDescription(QString::fromUtf8(fileReader.getDesc().c_str())); + // HACK + // Load order constraint of Bloodmoon.esm needing Tribunal.esm is missing + // from the file supplied by Bethesda, so we have to add it ourselves + if (file->fileName().compare("Bloodmoon.esm", Qt::CaseInsensitive) == 0) + { + file->addGameFile(QString::fromUtf8("Tribunal.esm")); + } // Put the file in the table - if (item(file->filePath()) == 0) - addFile(file); + addFile(file); } catch(std::runtime_error &e) { // An error occurred while reading the .esp @@ -476,11 +481,22 @@ void ContentSelectorModel::ContentModel::addFiles(const QString &path) } - delete decoder; - sortFiles(); } +QStringList ContentSelectorModel::ContentModel::gameFiles() const +{ + QStringList gameFiles; + foreach(const ContentSelectorModel::EsmFile *file, mFiles) + { + if (file->isGameFile()) + { + gameFiles.append(file->fileName()); + } + } + return gameFiles; +} + void ContentSelectorModel::ContentModel::sortFiles() { //first, sort the model such that all dependencies are ordered upstream (gamefile) first. @@ -501,7 +517,7 @@ void ContentSelectorModel::ContentModel::sortFiles() //dependencies appear. for (int j = i + 1; j < fileCount; j++) { - if (gamefiles.contains(mFiles.at(j)->fileName())) + if (gamefiles.contains(mFiles.at(j)->fileName(), Qt::CaseInsensitive)) { mFiles.move(j, i); @@ -531,11 +547,98 @@ bool ContentSelectorModel::ContentModel::isEnabled (QModelIndex index) const return (flags(index) & Qt::ItemIsEnabled); } -void ContentSelectorModel::ContentModel::setCheckStates (const QStringList &fileList, bool isChecked) +bool ContentSelectorModel::ContentModel::isLoadOrderError(const EsmFile *file) const +{ + return mPluginsWithLoadOrderError.contains(file->filePath()); +} + +void ContentSelectorModel::ContentModel::setContentList(const QStringList &fileList) +{ + mPluginsWithLoadOrderError.clear(); + int previousPosition = -1; + foreach (const QString &filepath, fileList) + { + if (setCheckState(filepath, true)) + { + // as necessary, move plug-ins in visible list to match sequence of supplied filelist + const EsmFile* file = item(filepath); + int filePosition = indexFromItem(file).row(); + if (filePosition < previousPosition) + { + mFiles.move(filePosition, previousPosition); + emit dataChanged(index(filePosition, 0, QModelIndex()), index(previousPosition, 0, QModelIndex())); + } + else + { + previousPosition = filePosition; + } + } + } + checkForLoadOrderErrors(); +} + +void ContentSelectorModel::ContentModel::checkForLoadOrderErrors() +{ + for (int row = 0; row < mFiles.count(); ++row) + { + EsmFile* file = item(row); + bool isRowInError = checkForLoadOrderErrors(file, row).count() != 0; + if (isRowInError) + { + mPluginsWithLoadOrderError.insert(file->filePath()); + } + else + { + mPluginsWithLoadOrderError.remove(file->filePath()); + } + } +} + +QList ContentSelectorModel::ContentModel::checkForLoadOrderErrors(const EsmFile *file, int row) const +{ + QList errors = QList(); + foreach(const QString &dependentfileName, file->gameFiles()) + { + const EsmFile* dependentFile = item(dependentfileName); + + if (!dependentFile) + { + errors.append(LoadOrderError(LoadOrderError::ErrorCode_MissingDependency, dependentfileName)); + } + else + { + if (!isChecked(dependentFile->filePath())) + { + errors.append(LoadOrderError(LoadOrderError::ErrorCode_InactiveDependency, dependentfileName)); + } + if (row < indexFromItem(dependentFile).row()) + { + errors.append(LoadOrderError(LoadOrderError::ErrorCode_LoadOrder, dependentfileName)); + } + } + } + return errors; +} + +QString ContentSelectorModel::ContentModel::toolTip(const EsmFile *file) const { - foreach (const QString &file, fileList) + if (isLoadOrderError(file)) + { + QString text(""); + int index = indexFromItem(item(file->filePath())).row(); + foreach(const LoadOrderError& error, checkForLoadOrderErrors(file, index)) + { + text += "

    "; + text += error.toolTip(); + text += "

    "; + } + text += ("
    "); + text += file->toolTip(); + return text; + } + else { - setCheckState (file, isChecked); + return file->toolTip(); } } @@ -590,7 +693,7 @@ bool ContentSelectorModel::ContentModel::setCheckState(const QString &filepath, QFileInfo fileInfo(filepath); QString filename = fileInfo.fileName(); - if (downstreamFile->gameFiles().contains(filename)) + if (downstreamFile->gameFiles().contains(filename, Qt::CaseInsensitive)) { if (mCheckStates.contains(downstreamFile->filePath())) mCheckStates[downstreamFile->filePath()] = Qt::Unchecked; diff --git a/components/contentselector/model/contentmodel.hpp b/components/contentselector/model/contentmodel.hpp index 7b2000b51..658555852 100644 --- a/components/contentselector/model/contentmodel.hpp +++ b/components/contentselector/model/contentmodel.hpp @@ -3,6 +3,9 @@ #include #include +#include +#include +#include "loadordererror.hpp" namespace ContentSelectorModel { @@ -20,7 +23,8 @@ namespace ContentSelectorModel { Q_OBJECT public: - explicit ContentModel(QObject *parent = 0); + explicit ContentModel(QObject *parent, QIcon warningIcon); + ~ContentModel(); void setEncoding(const QString &encoding); @@ -43,16 +47,20 @@ namespace ContentSelectorModel QModelIndex indexFromItem(const EsmFile *item) const; const EsmFile *item(const QString &name) const; + QStringList gameFiles() const; bool isEnabled (QModelIndex index) const; bool isChecked(const QString &filepath) const; bool setCheckState(const QString &filepath, bool isChecked); - void setCheckStates (const QStringList &fileList, bool isChecked); + void setContentList(const QStringList &fileList); ContentFileList checkedItems() const; void uncheckAll(); void refreshModel(); + /// Checks all plug-ins for load order errors and updates mPluginsWithLoadOrderError with plug-ins with issues + void checkForLoadOrderErrors(); + private: void addFile(EsmFile *file); @@ -61,17 +69,27 @@ namespace ContentSelectorModel void sortFiles(); + /// Checks a specific plug-in for load order errors + /// \return all errors found for specific plug-in + QList checkForLoadOrderErrors(const EsmFile *file, int row) const; + + /// \return true if plug-in has a Load Order error + bool isLoadOrderError(const EsmFile *file) const; + + QString toolTip(const EsmFile *file) const; + ContentFileList mFiles; QHash mCheckStates; + QSet mPluginsWithLoadOrderError; QTextCodec *mCodec; QString mEncoding; + QIcon mWarningIcon; public: QString mMimeType; QStringList mMimeTypes; int mColumnCount; - Qt::ItemFlags mDragDropFlags; Qt::DropActions mDropActions; }; } diff --git a/components/contentselector/model/esmfile.cpp b/components/contentselector/model/esmfile.cpp index a0a09105a..1ac1c7500 100644 --- a/components/contentselector/model/esmfile.cpp +++ b/components/contentselector/model/esmfile.cpp @@ -62,6 +62,13 @@ QByteArray ContentSelectorModel::EsmFile::encodedData() const return encodedData; } +bool ContentSelectorModel::EsmFile::isGameFile() const +{ + return (mGameFiles.size() == 0) && + (mFileName.endsWith(QLatin1String(".esm"), Qt::CaseInsensitive) || + mFileName.endsWith(QLatin1String(".omwgame"), Qt::CaseInsensitive)); +} + QVariant ContentSelectorModel::EsmFile::fileProperty(const FileProperty prop) const { switch (prop) diff --git a/components/contentselector/model/esmfile.hpp b/components/contentselector/model/esmfile.hpp index 7e1edcaba..d0cebab3c 100644 --- a/components/contentselector/model/esmfile.hpp +++ b/components/contentselector/model/esmfile.hpp @@ -64,7 +64,7 @@ namespace ContentSelectorModel .arg(mGameFiles.join(", ")); } - inline bool isGameFile() const { return (mGameFiles.size() == 0); } + bool isGameFile() const; QByteArray encodedData() const; public: diff --git a/components/contentselector/model/loadordererror.cpp b/components/contentselector/model/loadordererror.cpp new file mode 100644 index 000000000..aa69f330e --- /dev/null +++ b/components/contentselector/model/loadordererror.cpp @@ -0,0 +1,15 @@ +#include "loadordererror.hpp" +#include + +QString ContentSelectorModel::LoadOrderError::sErrorToolTips[ErrorCode_LoadOrder] = +{ + QString("Unable to find dependent file: %1"), + QString("Dependent file needs to be active: %1"), + QString("This file needs to load after %1") +}; + +QString ContentSelectorModel::LoadOrderError::toolTip() const +{ + assert(mErrorCode); + return sErrorToolTips[mErrorCode - 1].arg(mFileName); +} diff --git a/components/contentselector/model/loadordererror.hpp b/components/contentselector/model/loadordererror.hpp new file mode 100644 index 000000000..2b840cf69 --- /dev/null +++ b/components/contentselector/model/loadordererror.hpp @@ -0,0 +1,37 @@ +#ifndef LOADORDERERROR_HPP +#define LOADORDERERROR_HPP + +#include + +namespace ContentSelectorModel +{ + /// \brief Details of a suspected Load Order problem a plug-in will have. This is basically a POD. + class LoadOrderError + { + public: + enum ErrorCode + { + ErrorCode_None = 0, + ErrorCode_MissingDependency = 1, + ErrorCode_InactiveDependency = 2, + ErrorCode_LoadOrder = 3 + }; + + inline LoadOrderError() : mErrorCode(ErrorCode_None) {}; + inline LoadOrderError(ErrorCode errorCode, QString fileName) + { + mErrorCode = errorCode; + mFileName = fileName; + } + inline ErrorCode errorCode() const { return mErrorCode; } + inline QString fileName() const { return mFileName; } + QString toolTip() const; + + private: + ErrorCode mErrorCode; + QString mFileName; + static QString sErrorToolTips[ErrorCode_LoadOrder]; + }; +} + +#endif // LOADORDERERROR_HPP diff --git a/components/contentselector/view/combobox.cpp b/components/contentselector/view/combobox.cpp index 1d773b62d..18cafc2dc 100644 --- a/components/contentselector/view/combobox.cpp +++ b/components/contentselector/view/combobox.cpp @@ -30,7 +30,7 @@ void ContentSelectorView::ComboBox::paintEvent(QPaintEvent *) // draw the icon and text if (!opt.editable && currentIndex() == -1) // <<< we adjust the text displayed when nothing is selected opt.currentText = mPlaceholderText; - painter.drawControl(QStyle::CE_ComboBoxLabel, opt); + painter.drawControl(QStyle::CE_ComboBoxLabel, opt); } void ContentSelectorView::ComboBox::setPlaceholderText(const QString &text) diff --git a/components/contentselector/view/contentselector.cpp b/components/contentselector/view/contentselector.cpp index e9599de49..2363ae477 100644 --- a/components/contentselector/view/contentselector.cpp +++ b/components/contentselector/view/contentselector.cpp @@ -10,12 +10,14 @@ #include #include #include +#include #include ContentSelectorView::ContentSelector::ContentSelector(QWidget *parent) : QObject(parent) { - ui.setupUi (parent); + ui.setupUi(parent); + ui.addonView->setDragDropMode(QAbstractItemView::InternalMove); buildContentModel(); buildGameFileView(); @@ -24,20 +26,15 @@ ContentSelectorView::ContentSelector::ContentSelector(QWidget *parent) : void ContentSelectorView::ContentSelector::buildContentModel() { - mContentModel = new ContentSelectorModel::ContentModel(); + QIcon warningIcon(ui.addonView->style()->standardIcon(QStyle::SP_MessageBoxWarning).pixmap(QSize(16, 15))); + mContentModel = new ContentSelectorModel::ContentModel(this, warningIcon); } void ContentSelectorView::ContentSelector::buildGameFileView() { ui.gameFileView->setVisible (true); - mGameFileProxyModel = new QSortFilterProxyModel(this); - mGameFileProxyModel->setFilterRegExp(QString::number((int)ContentSelectorModel::ContentType_GameFile)); - mGameFileProxyModel->setFilterRole (Qt::UserRole); - mGameFileProxyModel->setSourceModel (mContentModel); - ui.gameFileView->setPlaceholderText(QString("Select a game file...")); - ui.gameFileView->setModel(mGameFileProxyModel); connect (ui.gameFileView, SIGNAL (currentIndexChanged(int)), this, SLOT (slotCurrentGameFileIndexChanged(int))); @@ -58,36 +55,25 @@ void ContentSelectorView::ContentSelector::buildAddonView() ui.addonView->setModel(mAddonProxyModel); - connect(ui.addonView, SIGNAL(clicked(const QModelIndex &)), this, SLOT(slotAddonTableItemClicked(const QModelIndex &))); + connect(ui.addonView, SIGNAL(activated(const QModelIndex&)), this, SLOT(slotAddonTableItemActivated(const QModelIndex&))); + connect(mContentModel, SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&)), this, SIGNAL(signalAddonDataChanged(QModelIndex,QModelIndex))); } void ContentSelectorView::ContentSelector::setProfileContent(const QStringList &fileList) { clearCheckStates(); - bool foundGamefile = false; foreach (const QString &filepath, fileList) { - if (!foundGamefile) + const ContentSelectorModel::EsmFile *file = mContentModel->item(filepath); + if (file && file->isGameFile()) { - const ContentSelectorModel::EsmFile *file = mContentModel->item(filepath); - - foundGamefile = (file->isGameFile()); - - if (foundGamefile) - { - setGameFile (filepath); - break; - } + setGameFile (filepath); + break; } } -/* if (!foundGameFile) - { - //throw gamefile error here. - }*/ - - setCheckStates (fileList); + setContentList(fileList); } void ContentSelectorView::ContentSelector::setGameFile(const QString &filename) @@ -115,14 +101,14 @@ void ContentSelectorView::ContentSelector::clearCheckStates() mContentModel->uncheckAll(); } -void ContentSelectorView::ContentSelector::setCheckStates(const QStringList &list) +void ContentSelectorView::ContentSelector::setContentList(const QStringList &list) { if (list.isEmpty()) { slotCurrentGameFileIndexChanged (ui.gameFileView->currentIndex()); } else - mContentModel->setCheckStates (list, true); + mContentModel->setContentList(list); } ContentSelectorModel::ContentFileList @@ -138,6 +124,15 @@ void ContentSelectorView::ContentSelector::addFiles(const QString &path) { mContentModel->addFiles(path); + // add any game files to the combo box + foreach(const QString gameFileName, mContentModel->gameFiles()) + { + if (ui.gameFileView->findText(gameFileName) == -1) + { + ui.gameFileView->addItem(gameFileName); + } + } + if (ui.gameFileView->currentIndex() != -1) ui.gameFileView->setCurrentIndex(-1); @@ -159,29 +154,34 @@ void ContentSelectorView::ContentSelector::slotCurrentGameFileIndexChanged(int i { static int oldIndex = -1; - QAbstractItemModel *const model = ui.gameFileView->model(); - QSortFilterProxyModel *proxy = dynamic_cast(model); - - if (proxy) - proxy->setDynamicSortFilter(false); - if (index != oldIndex) { if (oldIndex > -1) - model->setData(model->index(oldIndex, 0), false, Qt::UserRole + 1); + { + setGameFileSelected(oldIndex, false); + } oldIndex = index; - model->setData(model->index(index, 0), true, Qt::UserRole + 1); + setGameFileSelected(index, true); + mContentModel->checkForLoadOrderErrors(); } - if (proxy) - proxy->setDynamicSortFilter(true); - emit signalCurrentGamefileIndexChanged (index); } -void ContentSelectorView::ContentSelector::slotAddonTableItemClicked(const QModelIndex &index) +void ContentSelectorView::ContentSelector::setGameFileSelected(int index, bool selected) +{ + QString fileName = ui.gameFileView->itemText(index); + const ContentSelectorModel::EsmFile* file = mContentModel->item(fileName); + if (file != NULL) + { + QModelIndex index(mContentModel->indexFromItem(file)); + mContentModel->setData(index, selected, Qt::UserRole + 1); + } +} + +void ContentSelectorView::ContentSelector::slotAddonTableItemActivated(const QModelIndex &index) { QModelIndex sourceIndex = mAddonProxyModel->mapToSource (index); @@ -194,10 +194,4 @@ void ContentSelectorView::ContentSelector::slotAddonTableItemClicked(const QMode checkState = Qt::Checked; mContentModel->setData(sourceIndex, checkState, Qt::CheckStateRole); - - if (checkState == Qt::Checked) - emit signalAddonFileSelected (index.row()); - else - emit signalAddonFileUnselected (index.row()); - } diff --git a/components/contentselector/view/contentselector.hpp b/components/contentselector/view/contentselector.hpp index a25eb20ae..2507cf6ad 100644 --- a/components/contentselector/view/contentselector.hpp +++ b/components/contentselector/view/contentselector.hpp @@ -19,7 +19,6 @@ namespace ContentSelectorView protected: ContentSelectorModel::ContentModel *mContentModel; - QSortFilterProxyModel *mGameFileProxyModel; QSortFilterProxyModel *mAddonProxyModel; public: @@ -32,7 +31,7 @@ namespace ContentSelectorView void setProfileContent (const QStringList &fileList); void clearCheckStates(); - void setCheckStates (const QStringList &list); + void setContentList(const QStringList &list); ContentSelectorModel::ContentFileList selectedFiles() const; @@ -52,16 +51,17 @@ namespace ContentSelectorView void buildContentModel(); void buildGameFileView(); void buildAddonView(); + void setGameFileSelected(int index, bool selected); signals: void signalCurrentGamefileIndexChanged (int); - void signalAddonFileSelected (int); - void signalAddonFileUnselected (int); + + void signalAddonDataChanged (const QModelIndex& topleft, const QModelIndex& bottomright); private slots: void slotCurrentGameFileIndexChanged(int index); - void slotAddonTableItemClicked(const QModelIndex &index); + void slotAddonTableItemActivated(const QModelIndex& index); }; } diff --git a/components/esm/aipackage.cpp b/components/esm/aipackage.cpp index cf4951de7..efcd6651e 100644 --- a/components/esm/aipackage.cpp +++ b/components/esm/aipackage.cpp @@ -11,37 +11,34 @@ namespace ESM mServices = 0; } - void AIPackageList::load(ESMReader &esm) + void AIPackageList::add(ESMReader &esm) { - while (esm.hasMoreSubs()) { - // initialize every iteration - AIPackage pack; - esm.getSubName(); - if (esm.retSubName() == 0x54444e43) { // CNDT - mList.back().mCellName = esm.getHString(); - } else if (esm.retSubName() == AI_Wander) { - pack.mType = AI_Wander; - esm.getHExact(&pack.mWander, 14); - mList.push_back(pack); - } else if (esm.retSubName() == AI_Travel) { - pack.mType = AI_Travel; - esm.getHExact(&pack.mTravel, 16); - mList.push_back(pack); - } else if (esm.retSubName() == AI_Escort || - esm.retSubName() == AI_Follow) - { - pack.mType = - (esm.retSubName() == AI_Escort) ? AI_Escort : AI_Follow; - esm.getHExact(&pack.mTarget, 48); - mList.push_back(pack); - } else if (esm.retSubName() == AI_Activate) { - pack.mType = AI_Activate; - esm.getHExact(&pack.mActivate, 33); - mList.push_back(pack); - } else { // not AI package related data, so leave - return; - } + AIPackage pack; + if (esm.retSubName() == AI_CNDT) { + mList.back().mCellName = esm.getHString(); + } else if (esm.retSubName() == AI_Wander) { + pack.mType = AI_Wander; + esm.getHExact(&pack.mWander, 14); + mList.push_back(pack); + } else if (esm.retSubName() == AI_Travel) { + pack.mType = AI_Travel; + esm.getHExact(&pack.mTravel, 16); + mList.push_back(pack); + } else if (esm.retSubName() == AI_Escort || + esm.retSubName() == AI_Follow) + { + pack.mType = + (esm.retSubName() == AI_Escort) ? AI_Escort : AI_Follow; + esm.getHExact(&pack.mTarget, 48); + mList.push_back(pack); + } else if (esm.retSubName() == AI_Activate) { + pack.mType = AI_Activate; + esm.getHExact(&pack.mActivate, 33); + mList.push_back(pack); + } else { // not AI package related data, so leave + return; } + } void AIPackageList::save(ESMWriter &esm) const diff --git a/components/esm/aipackage.hpp b/components/esm/aipackage.hpp index 8a31aadf5..5e08806c8 100644 --- a/components/esm/aipackage.hpp +++ b/components/esm/aipackage.hpp @@ -16,8 +16,10 @@ namespace ESM struct AIData { - // These are probabilities - char mHello, mU1, mFight, mFlee, mAlarm, mU2, mU3, mU4; + unsigned char mHello; + char mU1; + unsigned char mFight, mFlee, mAlarm; // These are probabilities [0, 100] + char mU2, mU3, mU4; // Unknown values int mServices; // See the Services enum void blank(); @@ -61,7 +63,8 @@ namespace ESM AI_Travel = 0x545f4941, AI_Follow = 0x465f4941, AI_Escort = 0x455f4941, - AI_Activate = 0x415f4941 + AI_Activate = 0x415f4941, + AI_CNDT = 0x54444e43 }; /// \note Used for storaging packages in a single container @@ -88,11 +91,9 @@ namespace ESM { std::vector mList; - /// \note This breaks consistency of subrecords reading: - /// after calling it subrecord name is already read, so - /// it needs to use retSubName() if needed. But, hey, there - /// is only one field left (XSCL) and only two records uses AI - void load(ESMReader &esm); + /// Add a single AIPackage, assumes subrecord name was already read + void add(ESMReader &esm); + void save(ESMWriter &esm) const; }; } diff --git a/components/esm/aisequence.cpp b/components/esm/aisequence.cpp index 80440bdd3..5b36f9a4c 100644 --- a/components/esm/aisequence.cpp +++ b/components/esm/aisequence.cpp @@ -16,12 +16,20 @@ namespace AiSequence { esm.getHNT (mData, "DATA"); esm.getHNT(mStartTime, "STAR"); + mStoredInitialActorPosition = false; + if (esm.isNextSub("POS_")) + { + mStoredInitialActorPosition = true; + esm.getHT(mInitialActorPosition); + } } void AiWander::save(ESMWriter &esm) const { esm.writeHNT ("DATA", mData); esm.writeHNT ("STAR", mStartTime); + if (mStoredInitialActorPosition) + esm.writeHNT ("POS_", mInitialActorPosition); } void AiTravel::load(ESMReader &esm) @@ -58,6 +66,10 @@ namespace AiSequence esm.getHNT (mRemainingDuration, "DURA"); mCellId = esm.getHNOString ("CELL"); esm.getHNT (mAlwaysFollow, "ALWY"); + mCommanded = false; + esm.getHNOT (mCommanded, "CMND"); + mActive = false; + esm.getHNOT (mActive, "ACTV"); } void AiFollow::save(ESMWriter &esm) const @@ -68,6 +80,9 @@ namespace AiSequence if (!mCellId.empty()) esm.writeHNString ("CELL", mCellId); esm.writeHNT ("ALWY", mAlwaysFollow); + esm.writeHNT ("CMND", mCommanded); + if (mActive) + esm.writeHNT("ACTV", mActive); } void AiActivate::load(ESMReader &esm) diff --git a/components/esm/aisequence.hpp b/components/esm/aisequence.hpp index bf0e17fa0..2560fbe7d 100644 --- a/components/esm/aisequence.hpp +++ b/components/esm/aisequence.hpp @@ -6,6 +6,8 @@ #include "defs.hpp" +#include "util.hpp" + namespace ESM { class ESMReader; @@ -61,6 +63,9 @@ namespace ESM AiWanderData mData; ESM::TimeStamp mStartTime; + bool mStoredInitialActorPosition; + ESM::Vector3 mInitialActorPosition; + /// \todo add more AiWander state void load(ESMReader &esm); @@ -96,6 +101,9 @@ namespace ESM float mRemainingDuration; bool mAlwaysFollow; + bool mCommanded; + + bool mActive; void load(ESMReader &esm); void save(ESMWriter &esm) const; diff --git a/components/esm/cellref.cpp b/components/esm/cellref.cpp index 29d26d013..0dd2987b5 100644 --- a/components/esm/cellref.cpp +++ b/components/esm/cellref.cpp @@ -5,6 +5,12 @@ #include "esmwriter.hpp" void ESM::CellRef::load (ESMReader& esm, bool wideRefNum) +{ + loadId(esm, wideRefNum); + loadData(esm); +} + +void ESM::CellRef::loadId(ESMReader &esm, bool wideRefNum) { // According to Hrnchamd, this does not belong to the actual ref. Instead, it is a marker indicating that // the following refs are part of a "temp refs" section. A temp ref is not being tracked by the moved references system. @@ -19,7 +25,10 @@ void ESM::CellRef::load (ESMReader& esm, bool wideRefNum) esm.getHNT (mRefNum.mIndex, "FRMR"); mRefID = esm.getHNString ("NAME"); +} +void ESM::CellRef::loadData(ESMReader &esm) +{ // Again, UNAM sometimes appears after NAME and sometimes later. // Or perhaps this UNAM means something different? mReferenceBlocked = -1; @@ -37,12 +46,12 @@ void ESM::CellRef::load (ESMReader& esm, bool wideRefNum) esm.getHNOT (mFactionRank, "INDX"); mGoldValue = 1; - mCharge = -1; + mChargeInt = -1; mEnchantmentCharge = -1; esm.getHNOT (mEnchantmentCharge, "XCHG"); - esm.getHNOT (mCharge, "INTV"); + esm.getHNOT (mChargeInt, "INTV"); esm.getHNOT (mGoldValue, "NAM9"); @@ -97,8 +106,8 @@ void ESM::CellRef::save (ESMWriter &esm, bool wideRefNum, bool inInventory) cons if (mEnchantmentCharge != -1) esm.writeHNT("XCHG", mEnchantmentCharge); - if (mCharge != -1) - esm.writeHNT("INTV", mCharge); + if (mChargeInt != -1) + esm.writeHNT("INTV", mChargeInt); if (mGoldValue != 1) { esm.writeHNT("NAM9", mGoldValue); @@ -129,8 +138,7 @@ void ESM::CellRef::save (ESMWriter &esm, bool wideRefNum, bool inInventory) cons void ESM::CellRef::blank() { - mRefNum.mIndex = 0; - mRefNum.mContentFile = -1; + mRefNum.unset(); mRefID.clear(); mScale = 1; mOwner.clear(); @@ -138,14 +146,14 @@ void ESM::CellRef::blank() mSoul.clear(); mFaction.clear(); mFactionRank = -2; - mCharge = 0; - mEnchantmentCharge = 0; + mChargeInt = -1; + mEnchantmentCharge = -1; mGoldValue = 0; mDestCell.clear(); mLockLevel = 0; mKey.clear(); mTrap.clear(); - mReferenceBlocked = 0; + mReferenceBlocked = -1; mTeleport = false; for (int i=0; i<3; ++i) diff --git a/components/esm/cellref.hpp b/components/esm/cellref.hpp index 9c57061b0..01b6a8473 100644 --- a/components/esm/cellref.hpp +++ b/components/esm/cellref.hpp @@ -13,8 +13,12 @@ namespace ESM struct RefNum { - int mIndex; - int mContentFile; // -1 no content file + unsigned int mIndex; + int mContentFile; + + enum { RefNum_NoContentFile = -1 }; + inline bool hasContentFile() const { return mContentFile != RefNum_NoContentFile; } + inline void unset() { mIndex = 0; mContentFile = RefNum_NoContentFile; } }; /* Cell reference. This represents ONE object (of many) inside the @@ -55,7 +59,13 @@ namespace ESM // For weapon or armor, this is the remaining item health. // For tools (lockpicks, probes, repair hammer) it is the remaining uses. - int mCharge; + // For lights it is remaining time. + // This could be -1 if the charge was not touched yet (i.e. full). + union + { + int mChargeInt; // Used by everything except lights + float mChargeFloat; // Used only by lights + }; // Remaining enchantment charge. This could be -1 if the charge was not touched yet (i.e. full). float mEnchantmentCharge; @@ -85,8 +95,14 @@ namespace ESM // Position and rotation of this object within the cell Position mPos; + /// Calls loadId and loadData void load (ESMReader& esm, bool wideRefNum = false); + void loadId (ESMReader& esm, bool wideRefNum = false); + + /// Implicitly called by load + void loadData (ESMReader& esm); + void save (ESMWriter &esm, bool wideRefNum = false, bool inInventory = false) const; void blank(); diff --git a/components/esm/creaturestate.cpp b/components/esm/creaturestate.cpp index 9e9b56102..c15becd98 100644 --- a/components/esm/creaturestate.cpp +++ b/components/esm/creaturestate.cpp @@ -5,16 +5,28 @@ void ESM::CreatureState::load (ESMReader &esm) { ObjectState::load (esm); - mInventory.load (esm); + if (mHasCustomState) + { + mInventory.load (esm); - mCreatureStats.load (esm); + mCreatureStats.load (esm); + } } void ESM::CreatureState::save (ESMWriter &esm, bool inInventory) const { ObjectState::save (esm, inInventory); - mInventory.save (esm); + if (mHasCustomState) + { + mInventory.save (esm); - mCreatureStats.save (esm); -} \ No newline at end of file + mCreatureStats.save (esm); + } +} + +void ESM::CreatureState::blank() +{ + ObjectState::blank(); + mCreatureStats.blank(); +} diff --git a/components/esm/creaturestate.hpp b/components/esm/creaturestate.hpp index 604c2f3a7..9a3d41daa 100644 --- a/components/esm/creaturestate.hpp +++ b/components/esm/creaturestate.hpp @@ -14,6 +14,9 @@ namespace ESM InventoryState mInventory; CreatureStats mCreatureStats; + /// Initialize to default state + void blank(); + virtual void load (ESMReader &esm); virtual void save (ESMWriter &esm, bool inInventory = false) const; }; diff --git a/components/esm/creaturestats.cpp b/components/esm/creaturestats.cpp index 37f0cc63c..75c1c28bc 100644 --- a/components/esm/creaturestats.cpp +++ b/components/esm/creaturestats.cpp @@ -24,8 +24,8 @@ void ESM::CreatureStats::load (ESMReader &esm) mMurdered = false; esm.getHNOT (mMurdered, "MURD"); - mFriendlyHits = 0; - esm.getHNOT (mFriendlyHits, "FRHT"); + if (esm.isNextSub("FRHT")) + esm.skipHSub(); // Friendly hits, no longer used mTalkedTo = false; esm.getHNOT (mTalkedTo, "TALK"); @@ -36,8 +36,8 @@ void ESM::CreatureStats::load (ESMReader &esm) mAttacked = false; esm.getHNOT (mAttacked, "ATKD"); - mHostile = false; - esm.getHNOT (mHostile, "HOST"); + if (esm.isNextSub("HOST")) + esm.skipHSub(); // Hostile, no longer used mAttackingOrSpell = false; esm.getHNOT (mAttackingOrSpell, "ATCK"); @@ -68,6 +68,8 @@ void ESM::CreatureStats::load (ESMReader &esm) mLastHitObject = esm.getHNOString ("LHIT"); + mLastHitAttemptObject = esm.getHNOString ("LHAT"); + mRecalcDynamicStats = false; esm.getHNOT (mRecalcDynamicStats, "CALC"); @@ -86,14 +88,16 @@ void ESM::CreatureStats::load (ESMReader &esm) mSpells.load(esm); mActiveSpells.load(esm); mAiSequence.load(esm); + mMagicEffects.load(esm); while (esm.isNextSub("SUMM")) { int magicEffect; esm.getHT(magicEffect); + std::string source = esm.getHNOString("SOUR"); int actorId; esm.getHNT (actorId, "ACID"); - mSummonedCreatureMap[magicEffect] = actorId; + mSummonedCreatureMap[std::make_pair(magicEffect, source)] = actorId; } while (esm.isNextSub("GRAV")) @@ -136,9 +140,6 @@ void ESM::CreatureStats::save (ESMWriter &esm) const if (mMurdered) esm.writeHNT ("MURD", mMurdered); - if (mFriendlyHits) - esm.writeHNT ("FRHT", mFriendlyHits); - if (mTalkedTo) esm.writeHNT ("TALK", mTalkedTo); @@ -148,9 +149,6 @@ void ESM::CreatureStats::save (ESMWriter &esm) const if (mAttacked) esm.writeHNT ("ATKD", mAttacked); - if (mHostile) - esm.writeHNT ("HOST", mHostile); - if (mAttackingOrSpell) esm.writeHNT ("ATCK", mAttackingOrSpell); @@ -181,6 +179,9 @@ void ESM::CreatureStats::save (ESMWriter &esm) const if (!mLastHitObject.empty()) esm.writeHNString ("LHIT", mLastHitObject); + if (!mLastHitAttemptObject.empty()) + esm.writeHNString ("LHAT", mLastHitAttemptObject); + if (mRecalcDynamicStats) esm.writeHNT ("CALC", mRecalcDynamicStats); @@ -199,10 +200,12 @@ void ESM::CreatureStats::save (ESMWriter &esm) const mSpells.save(esm); mActiveSpells.save(esm); mAiSequence.save(esm); + mMagicEffects.save(esm); - for (std::map::const_iterator it = mSummonedCreatureMap.begin(); it != mSummonedCreatureMap.end(); ++it) + for (std::map, int>::const_iterator it = mSummonedCreatureMap.begin(); it != mSummonedCreatureMap.end(); ++it) { - esm.writeHNT ("SUMM", it->first); + esm.writeHNT ("SUMM", it->first.first); + esm.writeHNString ("SOUR", it->first.second); esm.writeHNT ("ACID", it->second); } @@ -212,6 +215,37 @@ void ESM::CreatureStats::save (ESMWriter &esm) const } esm.writeHNT("AISE", mHasAiSettings); - for (int i=0; i<4; ++i) - mAiSettings[i].save(esm); + if (mHasAiSettings) + { + for (int i=0; i<4; ++i) + mAiSettings[i].save(esm); + } +} + +void ESM::CreatureStats::blank() +{ + mTradeTime.mHour = 0; + mTradeTime.mDay = 0; + mGoldPool = 0; + mActorId = -1; + mHasAiSettings = false; + mDead = false; + mDied = false; + mMurdered = false; + mTalkedTo = false; + mAlarmed = false; + mAttacked = false; + mAttackingOrSpell = false; + mKnockdown = false; + mKnockdownOneFrame = false; + mKnockdownOverOneFrame = false; + mHitRecovery = false; + mBlock = false; + mMovementFlags = 0; + mAttackStrength = 0.f; + mFallHeight = 0.f; + mRecalcDynamicStats = false; + mDrawState = 0; + mDeathAnimation = 0; + mLevel = 1; } diff --git a/components/esm/creaturestats.hpp b/components/esm/creaturestats.hpp index 7ae57da16..2a03136d0 100644 --- a/components/esm/creaturestats.hpp +++ b/components/esm/creaturestats.hpp @@ -11,6 +11,7 @@ #include "spellstate.hpp" #include "activespells.hpp" +#include "magiceffects.hpp" #include "aisequence.hpp" namespace ESM @@ -24,12 +25,14 @@ namespace ESM StatState mAttributes[8]; StatState mDynamic[3]; + MagicEffects mMagicEffects; + AiSequence::AiSequence mAiSequence; bool mHasAiSettings; StatState mAiSettings[4]; - std::map mSummonedCreatureMap; + std::map, int> mSummonedCreatureMap; std::vector mSummonGraveyard; ESM::TimeStamp mTradeTime; @@ -39,11 +42,9 @@ namespace ESM bool mDead; bool mDied; bool mMurdered; - int mFriendlyHits; bool mTalkedTo; bool mAlarmed; bool mAttacked; - bool mHostile; bool mAttackingOrSpell; bool mKnockdown; bool mKnockdownOneFrame; @@ -54,6 +55,7 @@ namespace ESM float mAttackStrength; float mFallHeight; std::string mLastHitObject; + std::string mLastHitAttemptObject; bool mRecalcDynamicStats; int mDrawState; unsigned char mDeathAnimation; @@ -63,6 +65,9 @@ namespace ESM SpellState mSpells; ActiveSpells mActiveSpells; + /// Initialize to default state + void blank(); + void load (ESMReader &esm); void save (ESMWriter &esm) const; }; diff --git a/components/esm/custommarkerstate.cpp b/components/esm/custommarkerstate.cpp new file mode 100644 index 000000000..dc81c123d --- /dev/null +++ b/components/esm/custommarkerstate.cpp @@ -0,0 +1,26 @@ +#include "custommarkerstate.hpp" + +#include "esmwriter.hpp" +#include "esmreader.hpp" + +namespace ESM +{ + +void CustomMarker::save(ESM::ESMWriter &esm) const +{ + esm.writeHNT("POSX", mWorldX); + esm.writeHNT("POSY", mWorldY); + mCell.save(esm); + if (!mNote.empty()) + esm.writeHNString("NOTE", mNote); +} + +void CustomMarker::load(ESM::ESMReader &esm) +{ + esm.getHNT(mWorldX, "POSX"); + esm.getHNT(mWorldY, "POSY"); + mCell.load(esm); + mNote = esm.getHNOString("NOTE"); +} + +} diff --git a/components/esm/custommarkerstate.hpp b/components/esm/custommarkerstate.hpp new file mode 100644 index 000000000..fc9286bfe --- /dev/null +++ b/components/esm/custommarkerstate.hpp @@ -0,0 +1,30 @@ +#ifndef OPENMW_ESM_CUSTOMMARKERSTATE_H +#define OPENMW_ESM_CUSTOMMARKERSTATE_H + +#include "cellid.hpp" + +namespace ESM +{ + +// format 0, saved games only +struct CustomMarker +{ + float mWorldX; + float mWorldY; + + ESM::CellId mCell; + + std::string mNote; + + bool operator == (const CustomMarker& other) + { + return mNote == other.mNote && mCell == other.mCell && mWorldX == other.mWorldX && mWorldY == other.mWorldY; + } + + void load (ESM::ESMReader& reader); + void save (ESM::ESMWriter& writer) const; +}; + +} + +#endif diff --git a/components/esm/debugprofile.cpp b/components/esm/debugprofile.cpp new file mode 100644 index 000000000..6c05fac2a --- /dev/null +++ b/components/esm/debugprofile.cpp @@ -0,0 +1,29 @@ + +#include "debugprofile.hpp" + +#include "esmreader.hpp" +#include "esmwriter.hpp" +#include "defs.hpp" + +unsigned int ESM::DebugProfile::sRecordId = REC_DBGP; + +void ESM::DebugProfile::load (ESMReader& esm) +{ + mDescription = esm.getHNString ("DESC"); + mScriptText = esm.getHNString ("SCRP"); + esm.getHNT (mFlags, "FLAG"); +} + +void ESM::DebugProfile::save (ESMWriter& esm) const +{ + esm.writeHNCString ("DESC", mDescription); + esm.writeHNCString ("SCRP", mScriptText); + esm.writeHNT ("FLAG", mFlags); +} + +void ESM::DebugProfile::blank() +{ + mDescription.clear(); + mScriptText.clear(); + mFlags = 0; +} diff --git a/components/esm/debugprofile.hpp b/components/esm/debugprofile.hpp new file mode 100644 index 000000000..b54e8ff5f --- /dev/null +++ b/components/esm/debugprofile.hpp @@ -0,0 +1,38 @@ +#ifndef COMPONENTS_ESM_DEBUGPROFILE_H +#define COMPONENTS_ESM_DEBUGPROFILE_H + +#include + +namespace ESM +{ + class ESMReader; + class ESMWriter; + + struct DebugProfile + { + static unsigned int sRecordId; + + enum Flags + { + Flag_Default = 1, // add to newly opened scene subviews + Flag_BypassNewGame = 2, // bypass regular game startup + Flag_Global = 4 // make available from main menu (i.e. not location specific) + }; + + std::string mId; + + std::string mDescription; + + std::string mScriptText; + + unsigned int mFlags; + + void load (ESMReader& esm); + void save (ESMWriter& esm) const; + + /// Set record to default state (does not touch the ID). + void blank(); + }; +} + +#endif diff --git a/components/esm/defs.hpp b/components/esm/defs.hpp index f967af274..7ef8102c2 100644 --- a/components/esm/defs.hpp +++ b/components/esm/defs.hpp @@ -96,15 +96,17 @@ enum RecNameInts REC_WEAP = 0x50414557, // format 0 - saved games - REC_SAVE = 0x45564153, - REC_JOUR = 0x524f55a4, - REC_QUES = 0x53455551, - REC_GSCR = 0x52435347, - REC_PLAY = 0x59414c50, - REC_CSTA = 0x41545343, - REC_GMAP = 0x50414d47, - REC_DIAS = 0x53414944, - REC_WTHR = 0x52485457, + REC_SAVE = FourCC<'S','A','V','E'>::value, + REC_JOUR_LEGACY = FourCC<0xa4,'U','O','R'>::value, // "\xa4UOR", rather than "JOUR", little oversight when magic numbers were + // calculated by hand, needs to be supported for older files now + REC_JOUR = FourCC<'J','O','U','R'>::value, + REC_QUES = FourCC<'Q','U','E','S'>::value, + REC_GSCR = FourCC<'G','S','C','R'>::value, + REC_PLAY = FourCC<'P','L','A','Y'>::value, + REC_CSTA = FourCC<'C','S','T','A'>::value, + REC_GMAP = FourCC<'G','M','A','P'>::value, + REC_DIAS = FourCC<'D','I','A','S'>::value, + REC_WTHR = FourCC<'W','T','H','R'>::value, REC_KEYS = FourCC<'K','E','Y','S'>::value, REC_DYNA = FourCC<'D','Y','N','A'>::value, REC_ASPL = FourCC<'A','S','P','L'>::value, @@ -112,9 +114,14 @@ enum RecNameInts REC_MPRJ = FourCC<'M','P','R','J'>::value, REC_PROJ = FourCC<'P','R','O','J'>::value, REC_DCOU = FourCC<'D','C','O','U'>::value, + REC_MARK = FourCC<'M','A','R','K'>::value, + REC_ENAB = FourCC<'E','N','A','B'>::value, + REC_CAM_ = FourCC<'C','A','M','_'>::value, + REC_STLN = FourCC<'S','T','L','N'>::value, // format 1 - REC_FILT = 0x544C4946 + REC_FILT = FourCC<'F','I','L','T'>::value, + REC_DBGP = FourCC<'D','B','G','P'>::value ///< only used in project files }; } diff --git a/components/esm/dialoguestate.cpp b/components/esm/dialoguestate.cpp index 14301ac19..f302e36dc 100644 --- a/components/esm/dialoguestate.cpp +++ b/components/esm/dialoguestate.cpp @@ -13,13 +13,20 @@ void ESM::DialogueState::load (ESMReader &esm) { std::string faction = esm.getHString(); - while (esm.isNextSub ("REAC")) + while (esm.isNextSub("REA2")) { std::string faction2 = esm.getHString(); int reaction; esm.getHNT(reaction, "INTV"); + mChangedFactionReaction[faction][faction2] = reaction; + } - mModFactionReaction[faction][faction2] = reaction; + // no longer used + while (esm.isNextSub ("REAC")) + { + esm.skipHSub(); + esm.getSubName(); + esm.skipHSub(); } } } @@ -32,15 +39,15 @@ void ESM::DialogueState::save (ESMWriter &esm) const esm.writeHNString ("TOPI", *iter); } - for (std::map >::const_iterator iter = mModFactionReaction.begin(); - iter != mModFactionReaction.end(); ++iter) + for (std::map >::const_iterator iter = mChangedFactionReaction.begin(); + iter != mChangedFactionReaction.end(); ++iter) { esm.writeHNString ("FACT", iter->first); for (std::map::const_iterator reactIter = iter->second.begin(); reactIter != iter->second.end(); ++reactIter) { - esm.writeHNString ("REAC", reactIter->first); + esm.writeHNString ("REA2", reactIter->first); esm.writeHNT ("INTV", reactIter->second); } } diff --git a/components/esm/dialoguestate.hpp b/components/esm/dialoguestate.hpp index 5e5f602a3..d7cdb941c 100644 --- a/components/esm/dialoguestate.hpp +++ b/components/esm/dialoguestate.hpp @@ -14,9 +14,11 @@ namespace ESM struct DialogueState { + // must be lower case topic IDs std::vector mKnownTopics; - std::map > mModFactionReaction; + // must be lower case faction IDs + std::map > mChangedFactionReaction; void load (ESMReader &esm); void save (ESMWriter &esm) const; diff --git a/components/esm/effectlist.cpp b/components/esm/effectlist.cpp index bc126846b..f6d5a6e07 100644 --- a/components/esm/effectlist.cpp +++ b/components/esm/effectlist.cpp @@ -7,13 +7,19 @@ namespace ESM { void EffectList::load(ESMReader &esm) { - ENAMstruct s; + mList.clear(); while (esm.isNextSub("ENAM")) { - esm.getHT(s, 24); - mList.push_back(s); + add(esm); } } +void EffectList::add(ESMReader &esm) +{ + ENAMstruct s; + esm.getHT(s, 24); + mList.push_back(s); +} + void EffectList::save(ESMWriter &esm) const { for (std::vector::const_iterator it = mList.begin(); it != mList.end(); ++it) { diff --git a/components/esm/effectlist.hpp b/components/esm/effectlist.hpp index 04adcc5cd..d581f8337 100644 --- a/components/esm/effectlist.hpp +++ b/components/esm/effectlist.hpp @@ -29,11 +29,15 @@ namespace ESM }; #pragma pack(pop) + /// EffectList, ENAM subrecord struct EffectList { - std::vector mList; + /// Load one effect, assumes subrecord name was already read + void add(ESMReader &esm); + + /// Load all effects void load(ESMReader &esm); void save(ESMWriter &esm) const; }; diff --git a/components/esm/esmcommon.hpp b/components/esm/esmcommon.hpp index d3e6e7fea..54b18aeaf 100644 --- a/components/esm/esmcommon.hpp +++ b/components/esm/esmcommon.hpp @@ -23,7 +23,7 @@ template union NAME_T { char name[LEN]; - int32_t val; + uint32_t val; bool operator==(const char *str) const { @@ -40,8 +40,8 @@ union NAME_T } bool operator!=(const std::string &str) const { return !((*this)==str); } - bool operator==(int v) const { return v == val; } - bool operator!=(int v) const { return v != val; } + bool operator==(uint32_t v) const { return v == val; } + bool operator!=(uint32_t v) const { return v != val; } std::string toString() const { return std::string(name, strnlen(name, LEN)); } @@ -53,18 +53,6 @@ typedef NAME_T<32> NAME32; typedef NAME_T<64> NAME64; typedef NAME_T<256> NAME256; -#pragma pack(push) -#pragma pack(1) -// Data that is only present in save game files -struct SaveData -{ - float pos[6]; // Player position and rotation - NAME64 cell; // Cell name - float unk2; // Unknown value - possibly game time? - NAME32 player; // Player name -}; -#pragma pack(pop) - /* This struct defines a file 'context' which can be saved and later restored by an ESMReader instance. It will save the position within a file, and when restored will let you read from that position as diff --git a/components/esm/esmreader.cpp b/components/esm/esmreader.cpp index ebdb1e41f..bbe475ff7 100644 --- a/components/esm/esmreader.cpp +++ b/components/esm/esmreader.cpp @@ -123,7 +123,7 @@ std::string ESMReader::getHString() // Skip the following zero byte mCtx.leftRec--; char c; - mEsm->read(&c, 1); + getExact(&c, 1); return ""; } @@ -134,7 +134,11 @@ void ESMReader::getHExact(void*p, int size) { getSubHeader(); if (size != static_cast (mCtx.leftSub)) - fail("getHExact() size mismatch"); + { + std::stringstream error; + error << "getHExact(): size mismatch (requested " << size << ", got " << mCtx.leftSub << ")"; + fail(error.str()); + } getExact(p, size); } @@ -182,7 +186,7 @@ void ESMReader::getSubName() } // reading the subrecord data anyway. - mEsm->read(mCtx.subName.name, 4); + getExact(mCtx.subName.name, 4); mCtx.leftRec -= 4; } @@ -190,7 +194,7 @@ bool ESMReader::isEmptyOrGetName() { if (mCtx.leftRec) { - mEsm->read(mCtx.subName.name, 4); + getExact(mCtx.subName.name, 4); mCtx.leftRec -= 4; return false; } @@ -210,6 +214,17 @@ void ESMReader::skipHSubSize(int size) fail("skipHSubSize() mismatch"); } +void ESMReader::skipHSubUntil(const char *name) +{ + while (hasMoreSubs() && !isNextSub(name)) + { + mCtx.subCached = false; + skipHSub(); + } + if (hasMoreSubs()) + mCtx.subCached = true; +} + void ESMReader::getSubHeader() { if (mCtx.leftRec < 4) @@ -250,14 +265,6 @@ void ESMReader::skipRecord() mCtx.leftRec = 0; } -void ESMReader::skipHRecord() -{ - if (!mCtx.leftFile) - return; - getRecHeader(); - skipRecord(); -} - void ESMReader::getRecHeader(uint32_t &flags) { // General error checking @@ -287,9 +294,16 @@ void ESMReader::getRecHeader(uint32_t &flags) void ESMReader::getExact(void*x, int size) { - int t = mEsm->read(x, size); - if (t != size) - fail("Read error"); + try + { + int t = mEsm->read(x, size); + if (t != size) + fail("Read error"); + } + catch (std::exception& e) + { + fail(std::string("Read error: ") + e.what()); + } } std::string ESMReader::getString(int size) @@ -307,8 +321,7 @@ std::string ESMReader::getString(int size) char *ptr = &mBuffer[0]; getExact(ptr, size); - if (size>0 && ptr[size-1]==0) - --size; + size = strnlen(ptr, size); // Convert to UTF8 and return if (mEncoder) diff --git a/components/esm/esmreader.hpp b/components/esm/esmreader.hpp index 6b0bb9a27..ebbc935f6 100644 --- a/components/esm/esmreader.hpp +++ b/components/esm/esmreader.hpp @@ -32,10 +32,11 @@ public: int getVer() const { return mHeader.mData.version; } int getRecordCount() const { return mHeader.mData.records; } - float getFVer() const { if(mHeader.mData.version == VER_12) return 1.2; else return 1.3; } + float getFVer() const { return (mHeader.mData.version == VER_12) ? 1.2f : 1.3f; } const std::string getAuthor() const { return mHeader.mData.author.toString(); } const std::string getDesc() const { return mHeader.mData.desc.toString(); } const std::vector &getGameFiles() const { return mHeader.mMaster; } + const Header& getHeader() const { return mHeader; } int getFormat() const; const NAME &retSubName() const { return mCtx.subName; } uint32_t getSubSize() const { return mCtx.leftSub; } @@ -83,7 +84,7 @@ public: // indirectly to the load() method. int mIdx; void setIndex(const int index) {mIdx = index; mCtx.index = index;} - const int getIndex() {return mIdx;} + int getIndex() {return mIdx;} void setGlobalReaderList(std::vector *list) {mGlobalReaderList = list;} std::vector *getGlobalReaderList() {return mGlobalReaderList;} @@ -136,7 +137,11 @@ public: { getSubHeader(); if (mCtx.leftSub != sizeof(X)) - fail("getHT(): subrecord size mismatch"); + { + std::stringstream error; + error << "getHT(): subrecord size mismatch (requested " << sizeof(X) << ", got " << mCtx.leftSub << ")"; + fail(error.str()); + } getT(x); } @@ -194,6 +199,9 @@ public: // Skip sub record and check its size void skipHSubSize(int size); + // Skip all subrecords until the given subrecord or no more subrecords remaining + void skipHSubUntil(const char* name); + /* Sub-record header. This updates leftRec beyond the current sub-record as well. leftSub contains size of current sub-record. */ @@ -216,9 +224,6 @@ public: // already been read void skipRecord(); - // Skip an entire record, including the header (but not the name) - void skipHRecord(); - /* Read record header. This updatesleftFile BEYOND the data that follows the header, ie beyond the entire record. You should use leftRec to orient yourself inside the record itself. diff --git a/components/esm/esmwriter.cpp b/components/esm/esmwriter.cpp index 06572ce8f..14951608d 100644 --- a/components/esm/esmwriter.cpp +++ b/components/esm/esmwriter.cpp @@ -4,9 +4,16 @@ #include #include +#include + namespace ESM { - ESMWriter::ESMWriter() : mEncoder (0), mRecordCount (0), mCounting (true) {} + ESMWriter::ESMWriter() + : mEncoder (0) + , mRecordCount (0) + , mCounting (true) + , mStream(NULL) + {} unsigned int ESMWriter::getVersion() const { diff --git a/components/esm/esmwriter.hpp b/components/esm/esmwriter.hpp index e57c6e45d..30cec58b4 100644 --- a/components/esm/esmwriter.hpp +++ b/components/esm/esmwriter.hpp @@ -4,11 +4,14 @@ #include #include -#include - #include "esmcommon.hpp" #include "loadtes3.hpp" +namespace ToUTF8 +{ + class Utf8Encoder; +} + namespace ESM { class ESMWriter diff --git a/components/esm/globalmap.cpp b/components/esm/globalmap.cpp index f17c071ff..190329c61 100644 --- a/components/esm/globalmap.cpp +++ b/components/esm/globalmap.cpp @@ -21,7 +21,7 @@ void ESM::GlobalMap::load (ESMReader &esm) CellId cell; esm.getT(cell.first); esm.getT(cell.second); - mMarkers.push_back(cell); + mMarkers.insert(cell); } } @@ -33,7 +33,7 @@ void ESM::GlobalMap::save (ESMWriter &esm) const esm.write(&mImageData[0], mImageData.size()); esm.endRecord("DATA"); - for (std::vector::const_iterator it = mMarkers.begin(); it != mMarkers.end(); ++it) + for (std::set::const_iterator it = mMarkers.begin(); it != mMarkers.end(); ++it) { esm.startSubRecord("MRK_"); esm.writeT(it->first); diff --git a/components/esm/globalmap.hpp b/components/esm/globalmap.hpp index 158f70a6e..e89123f89 100644 --- a/components/esm/globalmap.hpp +++ b/components/esm/globalmap.hpp @@ -2,6 +2,7 @@ #define OPENMW_COMPONENTS_ESM_GLOBALMAP_H #include +#include namespace ESM { @@ -26,7 +27,7 @@ namespace ESM std::vector mImageData; typedef std::pair CellId; - std::vector mMarkers; + std::set mMarkers; void load (ESMReader &esm); void save (ESMWriter &esm) const; diff --git a/components/esm/globalscript.cpp b/components/esm/globalscript.cpp index dcbd91140..467fe54a1 100644 --- a/components/esm/globalscript.cpp +++ b/components/esm/globalscript.cpp @@ -12,6 +12,8 @@ void ESM::GlobalScript::load (ESMReader &esm) mRunning = 0; esm.getHNOT (mRunning, "RUN_"); + + mTargetId = esm.getHNOString ("TARG"); } void ESM::GlobalScript::save (ESMWriter &esm) const @@ -22,4 +24,6 @@ void ESM::GlobalScript::save (ESMWriter &esm) const if (mRunning) esm.writeHNT ("RUN_", mRunning); + + esm.writeHNOString ("TARG", mTargetId); } \ No newline at end of file diff --git a/components/esm/globalscript.hpp b/components/esm/globalscript.hpp index 4fb8b7c48..8b7e62795 100644 --- a/components/esm/globalscript.hpp +++ b/components/esm/globalscript.hpp @@ -12,9 +12,10 @@ namespace ESM struct GlobalScript { - std::string mId; + std::string mId; /// \note must be lowercase Locals mLocals; int mRunning; + std::string mTargetId; // for targeted scripts void load (ESMReader &esm); void save (ESMWriter &esm) const; diff --git a/components/esm/inventorystate.cpp b/components/esm/inventorystate.cpp index 2154faa83..4eaaa9f9f 100644 --- a/components/esm/inventorystate.cpp +++ b/components/esm/inventorystate.cpp @@ -4,52 +4,33 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -namespace -{ - void read (ESM::ESMReader &esm, ESM::ObjectState& state, int& slot) - { - slot = -1; - esm.getHNOT (slot, "SLOT"); - - state.load (esm); - } - - void write (ESM::ESMWriter &esm, const ESM::ObjectState& state, unsigned int type, int slot) - { - esm.writeHNT ("IOBJ", type); - - if (slot!=-1) - esm.writeHNT ("SLOT", slot); - - state.save (esm, true); - } -} - void ESM::InventoryState::load (ESMReader &esm) { + int index = 0; while (esm.isNextSub ("IOBJ")) { - unsigned int id = 0; - esm.getHT (id); + int unused; // no longer used + esm.getHT(unused); - if (id==ESM::REC_LIGH) - { - LightState state; - int slot; - read (esm, state, slot); - if (state.mCount == 0) - continue; - mLights.push_back (std::make_pair (state, slot)); - } - else + ObjectState state; + + // obsolete + if (esm.isNextSub("SLOT")) { - ObjectState state; int slot; - read (esm, state, slot); - if (state.mCount == 0) - continue; - mItems.push_back (std::make_pair (state, std::make_pair (id, slot))); + esm.getHT(slot); + mEquipmentSlots[index] = slot; } + + state.mRef.loadId(esm, true); + state.load (esm); + + if (state.mCount == 0) + continue; + + mItems.push_back (state); + + ++index; } while (esm.isNextSub("LEVM")) @@ -59,20 +40,72 @@ void ESM::InventoryState::load (ESMReader &esm) esm.getHNT (count, "COUN"); mLevelledItemMap[id] = count; } + + while (esm.isNextSub("MAGI")) + { + std::string id = esm.getHString(); + + std::vector > params; + while (esm.isNextSub("RAND")) + { + float rand, multiplier; + esm.getHT (rand); + esm.getHNT (multiplier, "MULT"); + params.push_back(std::make_pair(rand, multiplier)); + } + mPermanentMagicEffectMagnitudes[id] = params; + } + + while (esm.isNextSub("EQUI")) + { + esm.getSubHeader(); + int index; + esm.getT(index); + int slot; + esm.getT(slot); + mEquipmentSlots[index] = slot; + } + + mSelectedEnchantItem = -1; + esm.getHNOT(mSelectedEnchantItem, "SELE"); } void ESM::InventoryState::save (ESMWriter &esm) const { - for (std::vector > >::const_iterator iter (mItems.begin()); iter!=mItems.end(); ++iter) - write (esm, iter->first, iter->second.first, iter->second.second); + for (std::vector::const_iterator iter (mItems.begin()); iter!=mItems.end(); ++iter) + { + int unused = 0; + esm.writeHNT ("IOBJ", unused); - for (std::vector >::const_iterator iter (mLights.begin()); - iter!=mLights.end(); ++iter) - write (esm, iter->first, ESM::REC_LIGH, iter->second); + iter->save (esm, true); + } for (std::map::const_iterator it = mLevelledItemMap.begin(); it != mLevelledItemMap.end(); ++it) { esm.writeHNString ("LEVM", it->first); esm.writeHNT ("COUN", it->second); } + + for (TEffectMagnitudes::const_iterator it = mPermanentMagicEffectMagnitudes.begin(); it != mPermanentMagicEffectMagnitudes.end(); ++it) + { + esm.writeHNString("MAGI", it->first); + + const std::vector >& params = it->second; + for (std::vector >::const_iterator pIt = params.begin(); pIt != params.end(); ++pIt) + { + esm.writeHNT ("RAND", pIt->first); + esm.writeHNT ("MULT", pIt->second); + } + } + + for (std::map::const_iterator it = mEquipmentSlots.begin(); it != mEquipmentSlots.end(); ++it) + { + esm.startSubRecord("EQUI"); + esm.writeT(it->first); + esm.writeT(it->second); + esm.endRecord("EQUI"); + } + + if (mSelectedEnchantItem != -1) + esm.writeHNT ("SELE", mSelectedEnchantItem); } diff --git a/components/esm/inventorystate.hpp b/components/esm/inventorystate.hpp index 4aa79f575..d5c317beb 100644 --- a/components/esm/inventorystate.hpp +++ b/components/esm/inventorystate.hpp @@ -4,7 +4,6 @@ #include #include "objectstate.hpp" -#include "lightstate.hpp" namespace ESM { @@ -16,14 +15,19 @@ namespace ESM /// \brief State for inventories and containers struct InventoryState { - // anything but lights (type, slot) - std::vector > > mItems; + std::vector mItems; - // lights (slot) - std::vector > mLights; + // + std::map mEquipmentSlots; std::map mLevelledItemMap; + typedef std::map > > TEffectMagnitudes; + TEffectMagnitudes mPermanentMagicEffectMagnitudes; + + int mSelectedEnchantItem; // For inventories only + + InventoryState() : mSelectedEnchantItem(-1) {} virtual ~InventoryState() {} virtual void load (ESMReader &esm); diff --git a/components/esm/lightstate.cpp b/components/esm/lightstate.cpp deleted file mode 100644 index 1ef040823..000000000 --- a/components/esm/lightstate.cpp +++ /dev/null @@ -1,21 +0,0 @@ - -#include "lightstate.hpp" - -#include "esmreader.hpp" -#include "esmwriter.hpp" - -void ESM::LightState::load (ESMReader &esm) -{ - ObjectState::load (esm); - - mTime = 0; - esm.getHNOT (mTime, "LTIM"); -} - -void ESM::LightState::save (ESMWriter &esm, bool inInventory) const -{ - ObjectState::save (esm, inInventory); - - if (mTime) - esm.writeHNT ("LTIM", mTime); -} \ No newline at end of file diff --git a/components/esm/lightstate.hpp b/components/esm/lightstate.hpp deleted file mode 100644 index a22735e07..000000000 --- a/components/esm/lightstate.hpp +++ /dev/null @@ -1,19 +0,0 @@ -#ifndef OPENMW_ESM_LIGHTSTATE_H -#define OPENMW_ESM_LIGHTSTATE_H - -#include "objectstate.hpp" - -namespace ESM -{ - // format 0, saved games only - - struct LightState : public ObjectState - { - float mTime; - - virtual void load (ESMReader &esm); - virtual void save (ESMWriter &esm, bool inInventory = false) const; - }; -} - -#endif diff --git a/components/esm/loadacti.cpp b/components/esm/loadacti.cpp index 8efea3302..b5adce550 100644 --- a/components/esm/loadacti.cpp +++ b/components/esm/loadacti.cpp @@ -8,18 +8,34 @@ namespace ESM { unsigned int Activator::sRecordId = REC_ACTI; -void Activator::load(ESMReader &esm) -{ - mModel = esm.getHNString("MODL"); - mName = esm.getHNOString("FNAM"); - mScript = esm.getHNOString("SCRI"); -} -void Activator::save(ESMWriter &esm) const -{ - esm.writeHNCString("MODL", mModel); - esm.writeHNOCString("FNAM", mName); - esm.writeHNOCString("SCRI", mScript); -} + void Activator::load(ESMReader &esm) + { + while (esm.hasMoreSubs()) + { + esm.getSubName(); + uint32_t name = esm.retSubName().val; + switch (name) + { + case ESM::FourCC<'M','O','D','L'>::value: + mModel = esm.getHString(); + break; + case ESM::FourCC<'F','N','A','M'>::value: + mName = esm.getHString(); + break; + case ESM::FourCC<'S','C','R','I'>::value: + mScript = esm.getHString(); + break; + default: + esm.fail("Unknown subrecord"); + } + } + } + void Activator::save(ESMWriter &esm) const + { + esm.writeHNCString("MODL", mModel); + esm.writeHNOCString("FNAM", mName); + esm.writeHNOCString("SCRI", mScript); + } void Activator::blank() { diff --git a/components/esm/loadalch.cpp b/components/esm/loadalch.cpp index aac88482f..18db512c0 100644 --- a/components/esm/loadalch.cpp +++ b/components/esm/loadalch.cpp @@ -8,24 +8,51 @@ namespace ESM { unsigned int Potion::sRecordId = REC_ALCH; -void Potion::load(ESMReader &esm) -{ - mModel = esm.getHNOString("MODL"); - mIcon = esm.getHNOString("TEXT"); // not ITEX here for some reason - mScript = esm.getHNOString("SCRI"); - mName = esm.getHNOString("FNAM"); - esm.getHNT(mData, "ALDT", 12); - mEffects.load(esm); -} -void Potion::save(ESMWriter &esm) const -{ - esm.writeHNCString("MODL", mModel); - esm.writeHNOCString("TEXT", mIcon); - esm.writeHNOCString("SCRI", mScript); - esm.writeHNOCString("FNAM", mName); - esm.writeHNT("ALDT", mData, 12); - mEffects.save(esm); -} + void Potion::load(ESMReader &esm) + { + mEffects.mList.clear(); + bool hasData = false; + while (esm.hasMoreSubs()) + { + esm.getSubName(); + uint32_t name = esm.retSubName().val; + switch (name) + { + case ESM::FourCC<'M','O','D','L'>::value: + mModel = esm.getHString(); + break; + case ESM::FourCC<'T','E','X','T'>::value: // not ITEX here for some reason + mIcon = esm.getHString(); + break; + case ESM::FourCC<'S','C','R','I'>::value: + mScript = esm.getHString(); + break; + case ESM::FourCC<'F','N','A','M'>::value: + mName = esm.getHString(); + break; + case ESM::FourCC<'A','L','D','T'>::value: + esm.getHT(mData, 12); + hasData = true; + break; + case ESM::FourCC<'E','N','A','M'>::value: + mEffects.add(esm); + break; + default: + esm.fail("Unknown subrecord"); + } + } + if (!hasData) + esm.fail("Missing ALDT"); + } + void Potion::save(ESMWriter &esm) const + { + esm.writeHNCString("MODL", mModel); + esm.writeHNOCString("TEXT", mIcon); + esm.writeHNOCString("SCRI", mScript); + esm.writeHNOCString("FNAM", mName); + esm.writeHNT("ALDT", mData, 12); + mEffects.save(esm); + } void Potion::blank() { diff --git a/components/esm/loadappa.cpp b/components/esm/loadappa.cpp index 29ea78acc..f2c82aacf 100644 --- a/components/esm/loadappa.cpp +++ b/components/esm/loadappa.cpp @@ -10,25 +10,35 @@ namespace ESM void Apparatus::load(ESMReader &esm) { - // we will not treat duplicated subrecords as errors here + bool hasData = false; while (esm.hasMoreSubs()) { esm.getSubName(); - NAME subName = esm.retSubName(); - - if (subName == "MODL") - mModel = esm.getHString(); - else if (subName == "FNAM") - mName = esm.getHString(); - else if (subName == "AADT") - esm.getHT(mData); - else if (subName == "SCRI") - mScript = esm.getHString(); - else if (subName == "ITEX") - mIcon = esm.getHString(); - else - esm.fail("wrong subrecord type " + subName.toString() + " for APPA record"); + uint32_t name = esm.retSubName().val; + switch (name) + { + case ESM::FourCC<'M','O','D','L'>::value: + mModel = esm.getHString(); + break; + case ESM::FourCC<'F','N','A','M'>::value: + mName = esm.getHString(); + break; + case ESM::FourCC<'A','A','D','T'>::value: + esm.getHT(mData); + hasData = true; + break; + case ESM::FourCC<'S','C','R','I'>::value: + mScript = esm.getHString(); + break; + case ESM::FourCC<'I','T','E','X'>::value: + mIcon = esm.getHString(); + break; + default: + esm.fail("Unknown subrecord"); + } } + if (!hasData) + esm.fail("Missing AADT"); } void Apparatus::save(ESMWriter &esm) const diff --git a/components/esm/loadarmo.cpp b/components/esm/loadarmo.cpp index 5bf38c840..066551d6f 100644 --- a/components/esm/loadarmo.cpp +++ b/components/esm/loadarmo.cpp @@ -7,51 +7,87 @@ namespace ESM { -void PartReferenceList::load(ESMReader &esm) -{ - while (esm.isNextSub("INDX")) + void PartReferenceList::add(ESMReader &esm) { PartReference pr; esm.getHT(pr.mPart); // The INDX byte pr.mMale = esm.getHNOString("BNAM"); pr.mFemale = esm.getHNOString("CNAM"); mParts.push_back(pr); + } -} -void PartReferenceList::save(ESMWriter &esm) const -{ - for (std::vector::const_iterator it = mParts.begin(); it != mParts.end(); ++it) + void PartReferenceList::load(ESMReader &esm) { - esm.writeHNT("INDX", it->mPart); - esm.writeHNOString("BNAM", it->mMale); - esm.writeHNOString("CNAM", it->mFemale); + mParts.clear(); + while (esm.isNextSub("INDX")) + { + add(esm); + } } -} -unsigned int Armor::sRecordId = REC_ARMO; + void PartReferenceList::save(ESMWriter &esm) const + { + for (std::vector::const_iterator it = mParts.begin(); it != mParts.end(); ++it) + { + esm.writeHNT("INDX", it->mPart); + esm.writeHNOString("BNAM", it->mMale); + esm.writeHNOString("CNAM", it->mFemale); + } + } -void Armor::load(ESMReader &esm) -{ - mModel = esm.getHNString("MODL"); - mName = esm.getHNOString("FNAM"); - mScript = esm.getHNOString("SCRI"); - esm.getHNT(mData, "AODT", 24); - mIcon = esm.getHNOString("ITEX"); - mParts.load(esm); - mEnchant = esm.getHNOString("ENAM"); -} + unsigned int Armor::sRecordId = REC_ARMO; -void Armor::save(ESMWriter &esm) const -{ - esm.writeHNCString("MODL", mModel); - esm.writeHNOCString("FNAM", mName); - esm.writeHNOCString("SCRI", mScript); - esm.writeHNT("AODT", mData, 24); - esm.writeHNOCString("ITEX", mIcon); - mParts.save(esm); - esm.writeHNOCString("ENAM", mEnchant); -} + void Armor::load(ESMReader &esm) + { + mParts.mParts.clear(); + bool hasData = false; + while (esm.hasMoreSubs()) + { + esm.getSubName(); + uint32_t name = esm.retSubName().val; + switch (name) + { + case ESM::FourCC<'M','O','D','L'>::value: + mModel = esm.getHString(); + break; + case ESM::FourCC<'F','N','A','M'>::value: + mName = esm.getHString(); + break; + case ESM::FourCC<'A','O','D','T'>::value: + esm.getHT(mData, 24); + hasData = true; + break; + case ESM::FourCC<'S','C','R','I'>::value: + mScript = esm.getHString(); + break; + case ESM::FourCC<'I','T','E','X'>::value: + mIcon = esm.getHString(); + break; + case ESM::FourCC<'E','N','A','M'>::value: + mEnchant = esm.getHString(); + break; + case ESM::FourCC<'I','N','D','X'>::value: + mParts.add(esm); + break; + default: + esm.fail("Unknown subrecord"); + } + } + if (!hasData) + esm.fail("Missing CTDT subrecord"); + } + + void Armor::save(ESMWriter &esm) const + { + esm.writeHNCString("MODL", mModel); + esm.writeHNOCString("FNAM", mName); + esm.writeHNOCString("SCRI", mScript); + esm.writeHNT("AODT", mData, 24); + esm.writeHNOCString("ITEX", mIcon); + mParts.save(esm); + esm.writeHNOCString("ENAM", mEnchant); + } void Armor::blank() { diff --git a/components/esm/loadarmo.hpp b/components/esm/loadarmo.hpp index 991f4e185..356dfc1c5 100644 --- a/components/esm/loadarmo.hpp +++ b/components/esm/loadarmo.hpp @@ -46,7 +46,7 @@ enum PartReferenceType // Reference to body parts struct PartReference { - char mPart; + unsigned char mPart; // possible values [0, 26] std::string mMale, mFemale; }; @@ -55,6 +55,10 @@ struct PartReferenceList { std::vector mParts; + /// Load one part, assumes the subrecord name was already read + void add(ESMReader &esm); + + /// TODO: remove this method. The ESM format does not guarantee that all Part subrecords follow one another. void load(ESMReader &esm); void save(ESMWriter &esm) const; }; diff --git a/components/esm/loadbody.cpp b/components/esm/loadbody.cpp index 9a1164d04..ed24ded57 100644 --- a/components/esm/loadbody.cpp +++ b/components/esm/loadbody.cpp @@ -11,9 +11,30 @@ namespace ESM void BodyPart::load(ESMReader &esm) { - mModel = esm.getHNString("MODL"); - mRace = esm.getHNOString("FNAM"); - esm.getHNT(mData, "BYDT", 4); + bool hasData = false; + while (esm.hasMoreSubs()) + { + esm.getSubName(); + uint32_t name = esm.retSubName().val; + switch (name) + { + case ESM::FourCC<'M','O','D','L'>::value: + mModel = esm.getHString(); + break; + case ESM::FourCC<'F','N','A','M'>::value: + mRace = esm.getHString(); + break; + case ESM::FourCC<'B','Y','D','T'>::value: + esm.getHT(mData, 4); + hasData = true; + break; + default: + esm.fail("Unknown subrecord"); + } + } + + if (!hasData) + esm.fail("Missing BYDT subrecord"); } void BodyPart::save(ESMWriter &esm) const { diff --git a/components/esm/loadbody.hpp b/components/esm/loadbody.hpp index 286e3f96e..5e9869d24 100644 --- a/components/esm/loadbody.hpp +++ b/components/esm/loadbody.hpp @@ -49,10 +49,10 @@ struct BodyPart struct BYDTstruct { - char mPart; - char mVampire; - char mFlags; - char mType; + unsigned char mPart; // mesh part + unsigned char mVampire; // boolean + unsigned char mFlags; + unsigned char mType; // mesh type }; BYDTstruct mData; diff --git a/components/esm/loadbook.cpp b/components/esm/loadbook.cpp index c8b7e9478..47f52fc31 100644 --- a/components/esm/loadbook.cpp +++ b/components/esm/loadbook.cpp @@ -8,26 +8,54 @@ namespace ESM { unsigned int Book::sRecordId = REC_BOOK; -void Book::load(ESMReader &esm) -{ - mModel = esm.getHNString("MODL"); - mName = esm.getHNOString("FNAM"); - esm.getHNT(mData, "BKDT", 20); - mScript = esm.getHNOString("SCRI"); - mIcon = esm.getHNOString("ITEX"); - mText = esm.getHNOString("TEXT"); - mEnchant = esm.getHNOString("ENAM"); -} -void Book::save(ESMWriter &esm) const -{ - esm.writeHNCString("MODL", mModel); - esm.writeHNOCString("FNAM", mName); - esm.writeHNT("BKDT", mData, 20); - esm.writeHNOCString("SCRI", mScript); - esm.writeHNOCString("ITEX", mIcon); - esm.writeHNOString("TEXT", mText); - esm.writeHNOCString("ENAM", mEnchant); -} + void Book::load(ESMReader &esm) + { + bool hasData = false; + while (esm.hasMoreSubs()) + { + esm.getSubName(); + uint32_t name = esm.retSubName().val; + switch (name) + { + case ESM::FourCC<'M','O','D','L'>::value: + mModel = esm.getHString(); + break; + case ESM::FourCC<'F','N','A','M'>::value: + mName = esm.getHString(); + break; + case ESM::FourCC<'B','K','D','T'>::value: + esm.getHT(mData, 20); + hasData = true; + break; + case ESM::FourCC<'S','C','R','I'>::value: + mScript = esm.getHString(); + break; + case ESM::FourCC<'I','T','E','X'>::value: + mIcon = esm.getHString(); + break; + case ESM::FourCC<'E','N','A','M'>::value: + mEnchant = esm.getHString(); + break; + case ESM::FourCC<'T','E','X','T'>::value: + mText = esm.getHString(); + break; + default: + esm.fail("Unknown subrecord"); + } + } + if (!hasData) + esm.fail("Missing BKDT subrecord"); + } + void Book::save(ESMWriter &esm) const + { + esm.writeHNCString("MODL", mModel); + esm.writeHNOCString("FNAM", mName); + esm.writeHNT("BKDT", mData, 20); + esm.writeHNOCString("SCRI", mScript); + esm.writeHNOCString("ITEX", mIcon); + esm.writeHNOString("TEXT", mText); + esm.writeHNOCString("ENAM", mEnchant); + } void Book::blank() { diff --git a/components/esm/loadbsgn.cpp b/components/esm/loadbsgn.cpp index db1a72a36..e0cd83ea0 100644 --- a/components/esm/loadbsgn.cpp +++ b/components/esm/loadbsgn.cpp @@ -10,11 +10,29 @@ namespace ESM void BirthSign::load(ESMReader &esm) { - mName = esm.getHNOString("FNAM"); - mTexture = esm.getHNOString("TNAM"); - mDescription = esm.getHNOString("DESC"); - - mPowers.load(esm); + mPowers.mList.clear(); + while (esm.hasMoreSubs()) + { + esm.getSubName(); + uint32_t name = esm.retSubName().val; + switch (name) + { + case ESM::FourCC<'F','N','A','M'>::value: + mName = esm.getHString(); + break; + case ESM::FourCC<'T','N','A','M'>::value: + mTexture = esm.getHString(); + break; + case ESM::FourCC<'D','E','S','C'>::value: + mDescription = esm.getHString(); + break; + case ESM::FourCC<'N','P','C','S'>::value: + mPowers.add(esm); + break; + default: + esm.fail("Unknown subrecord"); + } + } } void BirthSign::save(ESMWriter &esm) const diff --git a/components/esm/loadcell.cpp b/components/esm/loadcell.cpp index 0a25fce84..e4f847dec 100644 --- a/components/esm/loadcell.cpp +++ b/components/esm/loadcell.cpp @@ -58,7 +58,7 @@ void Cell::load(ESMReader &esm, bool saveContext) void Cell::loadCell(ESMReader &esm, bool saveContext) { - mRefIdCounter = 0; + mRefNumCounter = 0; if (mData.mFlags & Interior) { @@ -92,7 +92,7 @@ void Cell::loadCell(ESMReader &esm, bool saveContext) esm.getHNOT(mMapColor, "NAM5"); } if (esm.isNextSub("NAM0")) { - esm.getHT(mRefIdCounter); + esm.getHT(mRefNumCounter); } if (saveContext) { @@ -113,11 +113,6 @@ void Cell::loadData(ESMReader &esm) esm.getHNT(mData, "DATA", 12); } -void Cell::preLoad(ESMReader &esm) //Can't be "load" because it conflicts with function in esmtool -{ - this->load(esm, false); -} - void Cell::postLoad(ESMReader &esm) { // Save position of the cell references and move on @@ -150,8 +145,8 @@ void Cell::save(ESMWriter &esm) const esm.writeHNT("NAM5", mMapColor); } - if (mRefIdCounter != 0) - esm.writeHNT("NAM0", mRefIdCounter); + if (mRefNumCounter != 0) + esm.writeHNT("NAM0", mRefNumCounter); } void Cell::restore(ESMReader &esm, int iCtx) const @@ -182,9 +177,9 @@ bool Cell::getNextRef(ESMReader &esm, CellRef &ref, bool& deleted) // NOTE: We should not need this check. It is a safety check until we have checked // more plugins, and how they treat these moved references. if (esm.isNextSub("MVRF")) { - esm.skipRecord(); // skip MVRF - esm.skipRecord(); // skip CNDT - // That should be it, I haven't seen any other fields yet. + // skip rest of cell record (moved references), they are handled elsewhere + esm.skipRecord(); // skip MVRF, CNDT + return false; } ref.load (esm); @@ -220,7 +215,7 @@ bool Cell::getNextMVRF(ESMReader &esm, MovedCellRef &mref) mWater = 0; mWaterInt = false; mMapColor = 0; - mRefIdCounter = 0; + mRefNumCounter = 0; mData.mFlags = 0; mData.mX = 0; @@ -247,6 +242,8 @@ bool Cell::getNextMVRF(ESMReader &esm, MovedCellRef &mref) else { id.mWorldspace = Misc::StringUtils::lowerCase (mName); + id.mIndex.mX = 0; + id.mIndex.mY = 0; } return id; diff --git a/components/esm/loadcell.hpp b/components/esm/loadcell.hpp index 5f889a55b..d982bbfd4 100644 --- a/components/esm/loadcell.hpp +++ b/components/esm/loadcell.hpp @@ -18,7 +18,7 @@ namespace ESM { class ESMReader; class ESMWriter; - class CellId; +struct CellId; /* Moved cell reference tracking object. This mainly stores the target cell of the reference, so we can easily know where it has been moved when another @@ -78,7 +78,13 @@ struct Cell float mFogDensity; }; - Cell() : mWater(0) {} + Cell() : mWater(0), + mName(""), + mRegion(""), + mWaterInt(false), + mMapColor(0), + mRefNumCounter(0) + {} // Interior cells are indexed by this (it's the 'id'), for exterior // cells it is optional. @@ -94,17 +100,16 @@ struct Cell float mWater; // Water level bool mWaterInt; int mMapColor; - // Counter for RefIds. This is only used during content file editing and has no impact on gameplay. - // It prevents overwriting previous refIDs, even if they were deleted. + // Counter for RefNums. This is only used during content file editing and has no impact on gameplay. + // It prevents overwriting previous refNums, even if they were deleted. // as that would collide with refs when a content file is upgraded. - int mRefIdCounter; + int mRefNumCounter; // References "leased" from another cell (i.e. a different cell // introduced this ref, and it has been moved here by a plugin) CellRefTracker mLeasedRefs; MovedCellRefTracker mMovedRefs; - void preLoad(ESMReader &esm); void postLoad(ESMReader &esm); // This method is left in for compatibility with esmtool. Parsing moved references currently requires @@ -132,7 +137,7 @@ struct Cell bool hasWater() const { - return (mData.mFlags&HasWater); + return (mData.mFlags&HasWater) != 0; } // Restore the given reader to the stored position. Will try to open diff --git a/components/esm/loadclas.cpp b/components/esm/loadclas.cpp index ec339bd15..66acaea72 100644 --- a/components/esm/loadclas.cpp +++ b/components/esm/loadclas.cpp @@ -10,17 +10,17 @@ namespace ESM { unsigned int Class::sRecordId = REC_CLAS; -const Class::Specialization Class::sSpecializationIds[3] = { - Class::Combat, - Class::Magic, - Class::Stealth -}; + const Class::Specialization Class::sSpecializationIds[3] = { + Class::Combat, + Class::Magic, + Class::Stealth + }; -const char *Class::sGmstSpecializationIds[3] = { - "sSpecializationCombat", - "sSpecializationMagic", - "sSpecializationStealth" -}; + const char *Class::sGmstSpecializationIds[3] = { + "sSpecializationCombat", + "sSpecializationMagic", + "sSpecializationStealth" + }; int& Class::CLDTstruct::getSkill (int index, bool major) @@ -39,22 +39,40 @@ const char *Class::sGmstSpecializationIds[3] = { return mSkills[index][major ? 1 : 0]; } -void Class::load(ESMReader &esm) -{ - mName = esm.getHNOString("FNAM"); - esm.getHNT(mData, "CLDT", 60); - - if (mData.mIsPlayable > 1) - esm.fail("Unknown bool value"); - - mDescription = esm.getHNOString("DESC"); -} -void Class::save(ESMWriter &esm) const -{ - esm.writeHNOCString("FNAM", mName); - esm.writeHNT("CLDT", mData, 60); - esm.writeHNOString("DESC", mDescription); -} + void Class::load(ESMReader &esm) + { + bool hasData = false; + while (esm.hasMoreSubs()) + { + esm.getSubName(); + uint32_t name = esm.retSubName().val; + switch (name) + { + case ESM::FourCC<'F','N','A','M'>::value: + mName = esm.getHString(); + break; + case ESM::FourCC<'C','L','D','T'>::value: + esm.getHT(mData, 60); + if (mData.mIsPlayable > 1) + esm.fail("Unknown bool value"); + hasData = true; + break; + case ESM::FourCC<'D','E','S','C'>::value: + mDescription = esm.getHString(); + break; + default: + esm.fail("Unknown subrecord"); + } + } + if (!hasData) + esm.fail("Missing CLDT subrecord"); + } + void Class::save(ESMWriter &esm) const + { + esm.writeHNOCString("FNAM", mName); + esm.writeHNT("CLDT", mData, 60); + esm.writeHNOString("DESC", mDescription); + } void Class::blank() { diff --git a/components/esm/loadclot.cpp b/components/esm/loadclot.cpp index 17ecdf3ae..5f49b5e70 100644 --- a/components/esm/loadclot.cpp +++ b/components/esm/loadclot.cpp @@ -10,17 +10,42 @@ namespace ESM void Clothing::load(ESMReader &esm) { - mModel = esm.getHNString("MODL"); - mName = esm.getHNOString("FNAM"); - esm.getHNT(mData, "CTDT", 12); - - mScript = esm.getHNOString("SCRI"); - mIcon = esm.getHNOString("ITEX"); - - mParts.load(esm); - - - mEnchant = esm.getHNOString("ENAM"); + mParts.mParts.clear(); + bool hasData = false; + while (esm.hasMoreSubs()) + { + esm.getSubName(); + uint32_t name = esm.retSubName().val; + switch (name) + { + case ESM::FourCC<'M','O','D','L'>::value: + mModel = esm.getHString(); + break; + case ESM::FourCC<'F','N','A','M'>::value: + mName = esm.getHString(); + break; + case ESM::FourCC<'C','T','D','T'>::value: + esm.getHT(mData, 12); + hasData = true; + break; + case ESM::FourCC<'S','C','R','I'>::value: + mScript = esm.getHString(); + break; + case ESM::FourCC<'I','T','E','X'>::value: + mIcon = esm.getHString(); + break; + case ESM::FourCC<'E','N','A','M'>::value: + mEnchant = esm.getHString(); + break; + case ESM::FourCC<'I','N','D','X'>::value: + mParts.add(esm); + break; + default: + esm.fail("Unknown subrecord"); + } + } + if (!hasData) + esm.fail("Missing CTDT subrecord"); } void Clothing::save(ESMWriter &esm) const diff --git a/components/esm/loadcont.cpp b/components/esm/loadcont.cpp index 7bdf9f05b..999c3f92a 100644 --- a/components/esm/loadcont.cpp +++ b/components/esm/loadcont.cpp @@ -7,54 +7,79 @@ namespace ESM { -void InventoryList::load(ESMReader &esm) -{ - ContItem ci; - while (esm.isNextSub("NPCO")) + void InventoryList::add(ESMReader &esm) { + ContItem ci; esm.getHT(ci, 36); mList.push_back(ci); } -} -void InventoryList::save(ESMWriter &esm) const -{ - for (std::vector::const_iterator it = mList.begin(); it != mList.end(); ++it) + void InventoryList::save(ESMWriter &esm) const { - esm.writeHNT("NPCO", *it, 36); + for (std::vector::const_iterator it = mList.begin(); it != mList.end(); ++it) + { + esm.writeHNT("NPCO", *it, 36); + } } -} unsigned int Container::sRecordId = REC_CONT; -void Container::load(ESMReader &esm) -{ - mModel = esm.getHNString("MODL"); - mName = esm.getHNOString("FNAM"); - esm.getHNT(mWeight, "CNDT", 4); - esm.getHNT(mFlags, "FLAG", 4); - - if (mFlags & 0xf4) - esm.fail("Unknown flags"); - if (!(mFlags & 0x8)) - esm.fail("Flag 8 not set"); - - mScript = esm.getHNOString("SCRI"); - - mInventory.load(esm); -} + void Container::load(ESMReader &esm) + { + mInventory.mList.clear(); + bool hasWeight = false; + bool hasFlags = false; + while (esm.hasMoreSubs()) + { + esm.getSubName(); + uint32_t name = esm.retSubName().val; + switch (name) + { + case ESM::FourCC<'M','O','D','L'>::value: + mModel = esm.getHString(); + break; + case ESM::FourCC<'F','N','A','M'>::value: + mName = esm.getHString(); + break; + case ESM::FourCC<'C','N','D','T'>::value: + esm.getHT(mWeight, 4); + hasWeight = true; + break; + case ESM::FourCC<'F','L','A','G'>::value: + esm.getHT(mFlags, 4); + if (mFlags & 0xf4) + esm.fail("Unknown flags"); + if (!(mFlags & 0x8)) + esm.fail("Flag 8 not set"); + hasFlags = true; + break; + case ESM::FourCC<'S','C','R','I'>::value: + mScript = esm.getHString(); + break; + case ESM::FourCC<'N','P','C','O'>::value: + mInventory.add(esm); + break; + default: + esm.fail("Unknown subrecord"); + } + } + if (!hasWeight) + esm.fail("Missing CNDT subrecord"); + if (!hasFlags) + esm.fail("Missing FLAG subrecord"); + } -void Container::save(ESMWriter &esm) const -{ - esm.writeHNCString("MODL", mModel); - esm.writeHNOCString("FNAM", mName); - esm.writeHNT("CNDT", mWeight, 4); - esm.writeHNT("FLAG", mFlags, 4); + void Container::save(ESMWriter &esm) const + { + esm.writeHNCString("MODL", mModel); + esm.writeHNOCString("FNAM", mName); + esm.writeHNT("CNDT", mWeight, 4); + esm.writeHNT("FLAG", mFlags, 4); - esm.writeHNOCString("SCRI", mScript); + esm.writeHNOCString("SCRI", mScript); - mInventory.save(esm); -} + mInventory.save(esm); + } void Container::blank() { diff --git a/components/esm/loadcont.hpp b/components/esm/loadcont.hpp index 2808b67b5..76c522d74 100644 --- a/components/esm/loadcont.hpp +++ b/components/esm/loadcont.hpp @@ -22,11 +22,14 @@ struct ContItem NAME32 mItem; }; +/// InventoryList, NPCO subrecord struct InventoryList { std::vector mList; - void load(ESMReader &esm); + /// Load one item, assumes subrecord name is already read + void add(ESMReader &esm); + void save(ESMWriter &esm) const; }; diff --git a/components/esm/loadcrea.cpp b/components/esm/loadcrea.cpp index 650de0801..2e9f924b7 100644 --- a/components/esm/loadcrea.cpp +++ b/components/esm/loadcrea.cpp @@ -8,55 +8,94 @@ namespace ESM { unsigned int Creature::sRecordId = REC_CREA; -void Creature::load(ESMReader &esm) -{ - mPersistent = esm.getRecordFlags() & 0x0400; - - mModel = esm.getHNString("MODL"); - mOriginal = esm.getHNOString("CNAM"); - mName = esm.getHNOString("FNAM"); - mScript = esm.getHNOString("SCRI"); - - esm.getHNT(mData, "NPDT", 96); - - esm.getHNT(mFlags, "FLAG"); - mScale = 1.0; - esm.getHNOT(mScale, "XSCL"); - - mInventory.load(esm); - mSpells.load(esm); - - if (esm.isNextSub("AIDT")) + void Creature::load(ESMReader &esm) { - esm.getHExact(&mAiData, sizeof(mAiData)); - mHasAI = true; - } - else - mHasAI = false; + mPersistent = esm.getRecordFlags() & 0x0400; - mAiPackage.load(esm); - esm.skipRecord(); -} + mAiPackage.mList.clear(); + mInventory.mList.clear(); + mSpells.mList.clear(); -void Creature::save(ESMWriter &esm) const -{ - esm.writeHNCString("MODL", mModel); - esm.writeHNOCString("CNAM", mOriginal); - esm.writeHNOCString("FNAM", mName); - esm.writeHNOCString("SCRI", mScript); - esm.writeHNT("NPDT", mData, 96); - esm.writeHNT("FLAG", mFlags); - if (mScale != 1.0) { - esm.writeHNT("XSCL", mScale); + mScale = 1.f; + mHasAI = false; + bool hasNpdt = false; + bool hasFlags = false; + while (esm.hasMoreSubs()) + { + esm.getSubName(); + uint32_t name = esm.retSubName().val; + switch (name) + { + case ESM::FourCC<'M','O','D','L'>::value: + mModel = esm.getHString(); + break; + case ESM::FourCC<'C','N','A','M'>::value: + mOriginal = esm.getHString(); + break; + case ESM::FourCC<'F','N','A','M'>::value: + mName = esm.getHString(); + break; + case ESM::FourCC<'S','C','R','I'>::value: + mScript = esm.getHString(); + break; + case ESM::FourCC<'N','P','D','T'>::value: + esm.getHT(mData, 96); + hasNpdt = true; + break; + case ESM::FourCC<'F','L','A','G'>::value: + esm.getHT(mFlags); + hasFlags = true; + break; + case ESM::FourCC<'X','S','C','L'>::value: + esm.getHT(mScale); + break; + case ESM::FourCC<'N','P','C','O'>::value: + mInventory.add(esm); + break; + case ESM::FourCC<'N','P','C','S'>::value: + mSpells.add(esm); + break; + case ESM::FourCC<'A','I','D','T'>::value: + esm.getHExact(&mAiData, sizeof(mAiData)); + mHasAI = true; + break; + case AI_Wander: + case AI_Activate: + case AI_Escort: + case AI_Follow: + case AI_Travel: + case AI_CNDT: + mAiPackage.add(esm); + break; + default: + esm.fail("Unknown subrecord"); + } + } + if (!hasNpdt) + esm.fail("Missing NPDT subrecord"); + if (!hasFlags) + esm.fail("Missing FLAG subrecord"); } - mInventory.save(esm); - mSpells.save(esm); - if (mHasAI) { - esm.writeHNT("AIDT", mAiData, sizeof(mAiData)); + void Creature::save(ESMWriter &esm) const + { + esm.writeHNCString("MODL", mModel); + esm.writeHNOCString("CNAM", mOriginal); + esm.writeHNOCString("FNAM", mName); + esm.writeHNOCString("SCRI", mScript); + esm.writeHNT("NPDT", mData, 96); + esm.writeHNT("FLAG", mFlags); + if (mScale != 1.0) { + esm.writeHNT("XSCL", mScale); + } + + mInventory.save(esm); + mSpells.save(esm); + if (mHasAI) { + esm.writeHNT("AIDT", mAiData, sizeof(mAiData)); + } + mAiPackage.save(esm); } - mAiPackage.save(esm); -} void Creature::blank() { diff --git a/components/esm/loadcrec.hpp b/components/esm/loadcrec.hpp deleted file mode 100644 index 280739aca..000000000 --- a/components/esm/loadcrec.hpp +++ /dev/null @@ -1,49 +0,0 @@ -#ifndef OPENMW_ESM_CREC_H -#define OPENMW_ESM_CREC_H - -#include - -// TODO create implementation files and remove this one -#include "esmreader.hpp" - -namespace ESM { - -class ESMReader; -class ESMWriter; - -/* These two are only used in save games. They are not decoded yet. - */ - -/// Changes a creature -struct LoadCREC -{ - static unsigned int sRecordId; - - std::string mId; - - void load(ESMReader &esm) - { - esm.skipRecord(); - } - - void save(ESMWriter &esm) const - { - } -}; - -/// Changes an item list / container -struct LoadCNTC -{ - std::string mId; - - void load(ESMReader &esm) - { - esm.skipRecord(); - } - - void save(ESMWriter &esm) const - { - } -}; -} -#endif diff --git a/components/esm/loaddial.cpp b/components/esm/loaddial.cpp index 92077b572..f2da8f377 100644 --- a/components/esm/loaddial.cpp +++ b/components/esm/loaddial.cpp @@ -63,6 +63,8 @@ void Dialogue::readInfo(ESMReader &esm, bool merge) std::map::iterator lookup; lookup = mLookup.find(id); + + ESM::DialInfo info; if (lookup != mLookup.end()) { it = lookup->second; @@ -70,13 +72,17 @@ void Dialogue::readInfo(ESMReader &esm, bool merge) // Merge with existing record. Only the subrecords that are present in // the new record will be overwritten. it->load(esm); - return; - } + info = *it; - // New record - ESM::DialInfo info; - info.mId = id; - info.load(esm); + // Since the record merging may have changed the next/prev linked list connection, we need to re-insert the record + mInfo.erase(it); + mLookup.erase(lookup); + } + else + { + info.mId = id; + info.load(esm); + } if (info.mNext.empty()) { @@ -110,4 +116,15 @@ void Dialogue::readInfo(ESMReader &esm, bool merge) std::cerr << "Failed to insert info " << id << std::endl; } +void Dialogue::clearDeletedInfos() +{ + for (InfoContainer::iterator it = mInfo.begin(); it != mInfo.end(); ) + { + if (it->mQuestStatus == DialInfo::QS_Deleted) + it = mInfo.erase(it); + else + ++it; + } +} + } diff --git a/components/esm/loaddial.hpp b/components/esm/loaddial.hpp index fd46ad210..d29948c63 100644 --- a/components/esm/loaddial.hpp +++ b/components/esm/loaddial.hpp @@ -47,6 +47,9 @@ struct Dialogue void load(ESMReader &esm); void save(ESMWriter &esm) const; + /// Remove all INFOs marked as QS_Deleted from mInfos. + void clearDeletedInfos(); + /// Read the next info record /// @param merge Merge with existing list, or just push each record to the end of the list? void readInfo (ESM::ESMReader& esm, bool merge); diff --git a/components/esm/loaddoor.cpp b/components/esm/loaddoor.cpp index c56b06337..f446eed61 100644 --- a/components/esm/loaddoor.cpp +++ b/components/esm/loaddoor.cpp @@ -8,23 +8,43 @@ namespace ESM { unsigned int Door::sRecordId = REC_DOOR; -void Door::load(ESMReader &esm) -{ - mModel = esm.getHNString("MODL"); - mName = esm.getHNOString("FNAM"); - mScript = esm.getHNOString("SCRI"); - mOpenSound = esm.getHNOString("SNAM"); - mCloseSound = esm.getHNOString("ANAM"); -} + void Door::load(ESMReader &esm) + { + while (esm.hasMoreSubs()) + { + esm.getSubName(); + uint32_t name = esm.retSubName().val; + switch (name) + { + case ESM::FourCC<'M','O','D','L'>::value: + mModel = esm.getHString(); + break; + case ESM::FourCC<'F','N','A','M'>::value: + mName = esm.getHString(); + break; + case ESM::FourCC<'S','C','R','I'>::value: + mScript = esm.getHString(); + break; + case ESM::FourCC<'S','N','A','M'>::value: + mOpenSound = esm.getHString(); + break; + case ESM::FourCC<'A','N','A','M'>::value: + mCloseSound = esm.getHString(); + break; + default: + esm.fail("Unknown subrecord"); + } + } + } -void Door::save(ESMWriter &esm) const -{ - esm.writeHNCString("MODL", mModel); - esm.writeHNOCString("FNAM", mName); - esm.writeHNOCString("SCRI", mScript); - esm.writeHNOCString("SNAM", mOpenSound); - esm.writeHNOCString("ANAM", mCloseSound); -} + void Door::save(ESMWriter &esm) const + { + esm.writeHNCString("MODL", mModel); + esm.writeHNOCString("FNAM", mName); + esm.writeHNOCString("SCRI", mScript); + esm.writeHNOCString("SNAM", mOpenSound); + esm.writeHNOCString("ANAM", mCloseSound); + } void Door::blank() { diff --git a/components/esm/loadench.cpp b/components/esm/loadench.cpp index 243803833..54690d9a0 100644 --- a/components/esm/loadench.cpp +++ b/components/esm/loadench.cpp @@ -10,8 +10,28 @@ namespace ESM void Enchantment::load(ESMReader &esm) { - esm.getHNT(mData, "ENDT", 16); - mEffects.load(esm); + mEffects.mList.clear(); + bool hasData = false; + while (esm.hasMoreSubs()) + { + esm.getSubName(); + uint32_t name = esm.retSubName().val; + switch (name) + { + case ESM::FourCC<'E','N','D','T'>::value: + esm.getHT(mData, 16); + hasData = true; + break; + case ESM::FourCC<'E','N','A','M'>::value: + mEffects.add(esm); + break; + default: + esm.fail("Unknown subrecord"); + break; + } + } + if (!hasData) + esm.fail("Missing ENDT subrecord"); } void Enchantment::save(ESMWriter &esm) const diff --git a/components/esm/loadfact.cpp b/components/esm/loadfact.cpp index db7e5b7b4..006ca0ce0 100644 --- a/components/esm/loadfact.cpp +++ b/components/esm/loadfact.cpp @@ -28,27 +28,46 @@ namespace ESM void Faction::load(ESMReader &esm) { - mName = esm.getHNOString("FNAM"); + mReactions.clear(); + for (int i=0;i<10;++i) + mRanks[i].clear(); - // Read rank names. These are optional. - int i = 0; - while (esm.isNextSub("RNAM") && i < 10) - mRanks[i++] = esm.getHString(); - - // Main data struct - esm.getHNT(mData, "FADT", 240); - - if (mData.mIsHidden > 1) - esm.fail("Unknown flag!"); - - // Read faction response values + int rankCounter=0; + bool hasData = false; while (esm.hasMoreSubs()) { - std::string faction = esm.getHNString("ANAM"); - int reaction; - esm.getHNT(reaction, "INTV"); - mReactions[faction] = reaction; + esm.getSubName(); + uint32_t name = esm.retSubName().val; + switch (name) + { + case ESM::FourCC<'F','N','A','M'>::value: + mName = esm.getHString(); + break; + case ESM::FourCC<'R','N','A','M'>::value: + if (rankCounter >= 10) + esm.fail("Rank out of range"); + mRanks[rankCounter++] = esm.getHString(); + break; + case ESM::FourCC<'F','A','D','T'>::value: + esm.getHT(mData, 240); + if (mData.mIsHidden > 1) + esm.fail("Unknown flag!"); + hasData = true; + break; + case ESM::FourCC<'A','N','A','M'>::value: + { + std::string faction = esm.getHString(); + int reaction; + esm.getHNT(reaction, "INTV"); + mReactions[faction] = reaction; + break; + } + default: + esm.fail("Unknown subrecord"); + } } + if (!hasData) + esm.fail("Missing FADT subrecord"); } void Faction::save(ESMWriter &esm) const { diff --git a/components/esm/loadinfo.cpp b/components/esm/loadinfo.cpp index c9860dcef..a2bade1c5 100644 --- a/components/esm/loadinfo.cpp +++ b/components/esm/loadinfo.cpp @@ -10,9 +10,15 @@ namespace ESM void DialInfo::load(ESMReader &esm) { + mQuestStatus = QS_None; + mFactionLess = false; + mPrev = esm.getHNString("PNAM"); mNext = esm.getHNString("NNAM"); + // Since there's no way to mark selects as "deleted", we have to clear the SelectStructs from all previous loadings + mSelects.clear(); + // Not present if deleted if (esm.isNextSub("DATA")) { esm.getHT(mData, 12); @@ -49,7 +55,6 @@ void DialInfo::load(ESMReader &esm) return; } - mFactionLess = false; if (subName.val == REC_FNAM) { mFaction = esm.getHString(); @@ -104,8 +109,6 @@ void DialInfo::load(ESMReader &esm) return; } - mQuestStatus = QS_None; - if (subName.val == REC_QSTN) mQuestStatus = QS_Name; else if (subName.val == REC_QSTF) diff --git a/components/esm/loadinfo.hpp b/components/esm/loadinfo.hpp index 0c0d662a8..59b1af31a 100644 --- a/components/esm/loadinfo.hpp +++ b/components/esm/loadinfo.hpp @@ -32,7 +32,11 @@ struct DialInfo struct DATAstruct { int mUnknown1; - int mDisposition; + union + { + int mDisposition; // Used for dialogue responses + int mJournalIndex; // Used for journal entries + }; signed char mRank; // Rank of NPC signed char mGender; // See Gender enum signed char mPCrank; // Player rank diff --git a/components/esm/loadingr.cpp b/components/esm/loadingr.cpp index 5c98cb8b9..7e0cc3168 100644 --- a/components/esm/loadingr.cpp +++ b/components/esm/loadingr.cpp @@ -8,45 +8,71 @@ namespace ESM { unsigned int Ingredient::sRecordId = REC_INGR; -void Ingredient::load(ESMReader &esm) -{ - mModel = esm.getHNString("MODL"); - mName = esm.getHNOString("FNAM"); - esm.getHNT(mData, "IRDT", 56); - mScript = esm.getHNOString("SCRI"); - mIcon = esm.getHNOString("ITEX"); - // horrible hack to fix broken data in records - for (int i=0; i<4; ++i) + void Ingredient::load(ESMReader &esm) { - if (mData.mEffectID[i] != 85 && - mData.mEffectID[i] != 22 && - mData.mEffectID[i] != 17 && - mData.mEffectID[i] != 79 && - mData.mEffectID[i] != 74) + bool hasData = false; + while (esm.hasMoreSubs()) { - mData.mAttributes[i] = -1; + esm.getSubName(); + uint32_t name = esm.retSubName().val; + switch (name) + { + case ESM::FourCC<'M','O','D','L'>::value: + mModel = esm.getHString(); + break; + case ESM::FourCC<'F','N','A','M'>::value: + mName = esm.getHString(); + break; + case ESM::FourCC<'I','R','D','T'>::value: + esm.getHT(mData, 56); + hasData = true; + break; + case ESM::FourCC<'S','C','R','I'>::value: + mScript = esm.getHString(); + break; + case ESM::FourCC<'I','T','E','X'>::value: + mIcon = esm.getHString(); + break; + default: + esm.fail("Unknown subrecord"); + } } - // is this relevant in cycle from 0 to 4? - if (mData.mEffectID[i] != 89 && - mData.mEffectID[i] != 26 && - mData.mEffectID[i] != 21 && - mData.mEffectID[i] != 83 && - mData.mEffectID[i] != 78) + if (!hasData) + esm.fail("Missing IRDT subrecord"); + + // horrible hack to fix broken data in records + for (int i=0; i<4; ++i) { - mData.mSkills[i] = -1; + if (mData.mEffectID[i] != 85 && + mData.mEffectID[i] != 22 && + mData.mEffectID[i] != 17 && + mData.mEffectID[i] != 79 && + mData.mEffectID[i] != 74) + { + mData.mAttributes[i] = -1; + } + + // is this relevant in cycle from 0 to 4? + if (mData.mEffectID[i] != 89 && + mData.mEffectID[i] != 26 && + mData.mEffectID[i] != 21 && + mData.mEffectID[i] != 83 && + mData.mEffectID[i] != 78) + { + mData.mSkills[i] = -1; + } } } -} -void Ingredient::save(ESMWriter &esm) const -{ - esm.writeHNCString("MODL", mModel); - esm.writeHNOCString("FNAM", mName); - esm.writeHNT("IRDT", mData, 56); - esm.writeHNOCString("SCRI", mScript); - esm.writeHNOCString("ITEX", mIcon); -} + void Ingredient::save(ESMWriter &esm) const + { + esm.writeHNCString("MODL", mModel); + esm.writeHNOCString("FNAM", mName); + esm.writeHNT("IRDT", mData, 56); + esm.writeHNOCString("SCRI", mScript); + esm.writeHNOCString("ITEX", mIcon); + } void Ingredient::blank() { diff --git a/components/esm/loadland.cpp b/components/esm/loadland.cpp index 1b701229e..ae73eee52 100644 --- a/components/esm/loadland.cpp +++ b/components/esm/loadland.cpp @@ -19,14 +19,14 @@ void Land::LandData::save(ESMWriter &esm) offsets.mUnk1 = mUnk1; offsets.mUnk2 = mUnk2; - float prevY = mHeights[0], prevX; + float prevY = mHeights[0]; int number = 0; // avoid multiplication for (int i = 0; i < LAND_SIZE; ++i) { float diff = (mHeights[number] - prevY) / HEIGHT_SCALE; offsets.mHeightData[number] = (diff >= 0) ? (int8_t) (diff + 0.5) : (int8_t) (diff - 0.5); - prevX = prevY = mHeights[number]; + float prevX = prevY = mHeights[number]; ++number; for (int j = 1; j < LAND_SIZE; ++j) { @@ -72,7 +72,6 @@ Land::Land() , mDataLoaded(false) , mLandData(NULL) , mPlugin(0) - , mHasData(false) { } @@ -97,8 +96,6 @@ void Land::load(ESMReader &esm) // Store the file position mContext = esm.getContext(); - mHasData = false; - // Skip these here. Load the actual data when the cell is loaded. if (esm.isNextSub("VNML")) { @@ -126,10 +123,6 @@ void Land::load(ESMReader &esm) mDataTypes |= DATA_VTEX; } - // We need all three of VNML, VHGT and VTEX in order to use the - // landscape. (Though Morrowind seems to accept terrain without VTEX/VCLR entries) - mHasData = mDataTypes & (DATA_VNML|DATA_VHGT|DATA_WNAM); - mDataLoaded = 0; mLandData = NULL; } @@ -144,13 +137,12 @@ void Land::save(ESMWriter &esm) const esm.writeHNT("DATA", mFlags); } -/// \todo remove memory allocation when only defaults needed void Land::loadData(int flags) { // Try to load only available data - int actual = flags & mDataTypes; + flags = flags & mDataTypes; // Return if all required data is loaded - if (flags == 0 || (actual != 0 && (mDataLoaded & actual) == actual)) { + if ((mDataLoaded & flags) == flags) { return; } // Create storage if nothing is loaded @@ -160,15 +152,13 @@ void Land::loadData(int flags) } mEsm->restoreContext(mContext); - memset(mLandData->mNormals, 0, sizeof(mLandData->mNormals)); - if (mEsm->isNextSub("VNML")) { - condLoad(actual, DATA_VNML, mLandData->mNormals, sizeof(mLandData->mNormals)); + condLoad(flags, DATA_VNML, mLandData->mNormals, sizeof(mLandData->mNormals)); } if (mEsm->isNextSub("VHGT")) { static VHGT vhgt; - if (condLoad(actual, DATA_VHGT, &vhgt, sizeof(vhgt))) { + if (condLoad(flags, DATA_VHGT, &vhgt, sizeof(vhgt))) { float rowOffset = vhgt.mHeightOffset; for (int y = 0; y < LAND_SIZE; y++) { rowOffset += vhgt.mHeightData[y * LAND_SIZE]; @@ -184,30 +174,18 @@ void Land::loadData(int flags) mLandData->mUnk1 = vhgt.mUnk1; mLandData->mUnk2 = vhgt.mUnk2; } - } else if ((flags & DATA_VHGT) && (mDataLoaded & DATA_VHGT) == 0) { - for (int i = 0; i < LAND_NUM_VERTS; ++i) { - mLandData->mHeights[i] = -256.0f * HEIGHT_SCALE; - } - mDataLoaded |= DATA_VHGT; } if (mEsm->isNextSub("WNAM")) { - condLoad(actual, DATA_WNAM, mLandData->mWnam, 81); - } - if (mEsm->isNextSub("VCLR")) { - mLandData->mUsingColours = true; - condLoad(actual, DATA_VCLR, mLandData->mColours, 3 * LAND_NUM_VERTS); - } else { - mLandData->mUsingColours = false; + condLoad(flags, DATA_WNAM, mLandData->mWnam, 81); } + if (mEsm->isNextSub("VCLR")) + condLoad(flags, DATA_VCLR, mLandData->mColours, 3 * LAND_NUM_VERTS); if (mEsm->isNextSub("VTEX")) { static uint16_t vtex[LAND_NUM_TEXTURES]; - if (condLoad(actual, DATA_VTEX, vtex, sizeof(vtex))) { + if (condLoad(flags, DATA_VTEX, vtex, sizeof(vtex))) { LandData::transposeTextureData(vtex, mLandData->mTextures); } - } else if ((flags & DATA_VTEX) && (mDataLoaded & DATA_VTEX) == 0) { - memset(mLandData->mTextures, 0, sizeof(mLandData->mTextures)); - mDataLoaded |= DATA_VTEX; } } @@ -232,4 +210,9 @@ bool Land::condLoad(int flags, int dataFlag, void *ptr, unsigned int size) return false; } +bool Land::isDataLoaded(int flags) const +{ + return (mDataLoaded & flags) == (flags & mDataTypes); +} + } diff --git a/components/esm/loadland.hpp b/components/esm/loadland.hpp index 028341ced..e510616af 100644 --- a/components/esm/loadland.hpp +++ b/components/esm/loadland.hpp @@ -32,7 +32,6 @@ struct Land ESMReader* mEsm; ESM_Context mContext; - bool mHasData; int mDataTypes; int mDataLoaded; @@ -81,11 +80,12 @@ struct Land VNML mNormals[LAND_NUM_VERTS * 3]; uint16_t mTextures[LAND_NUM_TEXTURES]; - bool mUsingColours; char mColours[3 * LAND_NUM_VERTS]; int mDataTypes; - uint8_t mWnam[81]; + // low-LOD heightmap (used for rendering the global map) + signed char mWnam[81]; + short mUnk1; uint8_t mUnk2; @@ -98,6 +98,8 @@ struct Land void load(ESMReader &esm); void save(ESMWriter &esm) const; + void blank() {} + /** * Actually loads data */ @@ -109,10 +111,8 @@ struct Land void unloadData(); /// Check if given data type is loaded - /// \todo reimplement this - bool isDataLoaded(int flags) { - return (mDataLoaded & flags) == flags; - } + /// @note We only check data types that *can* be loaded (present in mDataTypes) + bool isDataLoaded(int flags) const; private: Land(const Land& land); diff --git a/components/esm/loadlevlist.cpp b/components/esm/loadlevlist.cpp index 6385b9a71..ca5c5d74d 100644 --- a/components/esm/loadlevlist.cpp +++ b/components/esm/loadlevlist.cpp @@ -7,47 +7,53 @@ namespace ESM { -void LeveledListBase::load(ESMReader &esm) -{ - esm.getHNT(mFlags, "DATA"); - esm.getHNT(mChanceNone, "NNAM"); - - if (esm.isNextSub("INDX")) + void LevelledListBase::load(ESMReader &esm) { - int len; - esm.getHT(len); - mList.resize(len); - } - else - return; + esm.getHNT(mFlags, "DATA"); + esm.getHNT(mChanceNone, "NNAM"); - // TODO: Merge with an existing lists here. This can be done - // simply by adding the lists together, making sure that they are - // sorted by level. A better way might be to exclude repeated - // items. Also, some times we don't want to merge lists, just - // overwrite. Figure out a way to give the user this option. + if (esm.isNextSub("INDX")) + { + int len; + esm.getHT(len); + mList.resize(len); + } + else + { + // Original engine ignores rest of the record, even if there are items following + mList.clear(); + esm.skipRecord(); + return; + } - for (size_t i = 0; i < mList.size(); i++) - { - LevelItem &li = mList[i]; - li.mId = esm.getHNString(mRecName); - esm.getHNT(li.mLevel, "INTV"); - } -} -void LeveledListBase::save(ESMWriter &esm) const -{ - esm.writeHNT("DATA", mFlags); - esm.writeHNT("NNAM", mChanceNone); - esm.writeHNT("INDX", mList.size()); + // If this levelled list was already loaded by a previous content file, + // we overwrite the list. Merging lists should probably be left to external tools, + // with the limited amount of information there is in the records, all merging methods + // will be flawed in some way. For a proper fix the ESM format would have to be changed + // to actually track list changes instead of including the whole list for every file + // that does something with that list. - for (std::vector::const_iterator it = mList.begin(); it != mList.end(); ++it) + for (size_t i = 0; i < mList.size(); i++) + { + LevelItem &li = mList[i]; + li.mId = esm.getHNString(mRecName); + esm.getHNT(li.mLevel, "INTV"); + } + } + void LevelledListBase::save(ESMWriter &esm) const { - esm.writeHNCString(mRecName, it->mId); - esm.writeHNT("INTV", it->mLevel); + esm.writeHNT("DATA", mFlags); + esm.writeHNT("NNAM", mChanceNone); + esm.writeHNT("INDX", mList.size()); + + for (std::vector::const_iterator it = mList.begin(); it != mList.end(); ++it) + { + esm.writeHNCString(mRecName, it->mId); + esm.writeHNT("INTV", it->mLevel); + } } -} - void LeveledListBase::blank() + void LevelledListBase::blank() { mFlags = 0; mChanceNone = 0; diff --git a/components/esm/loadlevlist.hpp b/components/esm/loadlevlist.hpp index a4e1b85c2..bcea2b234 100644 --- a/components/esm/loadlevlist.hpp +++ b/components/esm/loadlevlist.hpp @@ -11,14 +11,14 @@ class ESMReader; class ESMWriter; /* - * Leveled lists. Since these have identical layout, I only bothered + * Levelled lists. Since these have identical layout, I only bothered * to implement it once. * - * We should later implement the ability to merge leveled lists from + * We should later implement the ability to merge levelled lists from * several files. */ -struct LeveledListBase +struct LevelledListBase { int mFlags; unsigned char mChanceNone; // Chance that none are selected (0-100) @@ -43,7 +43,7 @@ struct LeveledListBase ///< Set record to default state (does not touch the ID). }; -struct CreatureLevList: LeveledListBase +struct CreatureLevList: LevelledListBase { static unsigned int sRecordId; @@ -61,7 +61,7 @@ struct CreatureLevList: LeveledListBase } }; -struct ItemLevList: LeveledListBase +struct ItemLevList: LevelledListBase { static unsigned int sRecordId; @@ -72,7 +72,7 @@ struct ItemLevList: LeveledListBase // list is instantiated, instead of // giving several identical items // (used when a container has more - // than one instance of one leveled + // than one instance of one levelled // list.) AllLevels = 0x02 // Calculate from all levels <= player // level, not just the closest below diff --git a/components/esm/loadligh.cpp b/components/esm/loadligh.cpp index c02bb46b6..26d70d964 100644 --- a/components/esm/loadligh.cpp +++ b/components/esm/loadligh.cpp @@ -8,25 +8,50 @@ namespace ESM { unsigned int Light::sRecordId = REC_LIGH; -void Light::load(ESMReader &esm) -{ - mModel = esm.getHNString("MODL"); - mName = esm.getHNOString("FNAM"); - mIcon = esm.getHNOString("ITEX"); - assert(sizeof(mData) == 24); - esm.getHNT(mData, "LHDT", 24); - mScript = esm.getHNOString("SCRI"); - mSound = esm.getHNOString("SNAM"); -} -void Light::save(ESMWriter &esm) const -{ - esm.writeHNCString("MODL", mModel); - esm.writeHNOCString("FNAM", mName); - esm.writeHNOCString("ITEX", mIcon); - esm.writeHNT("LHDT", mData, 24); - esm.writeHNOCString("SCRI", mScript); - esm.writeHNOCString("SNAM", mSound); -} + void Light::load(ESMReader &esm) + { + bool hasData = false; + while (esm.hasMoreSubs()) + { + esm.getSubName(); + uint32_t name = esm.retSubName().val; + switch (name) + { + case ESM::FourCC<'M','O','D','L'>::value: + mModel = esm.getHString(); + break; + case ESM::FourCC<'F','N','A','M'>::value: + mName = esm.getHString(); + break; + case ESM::FourCC<'I','T','E','X'>::value: + mIcon = esm.getHString(); + break; + case ESM::FourCC<'L','H','D','T'>::value: + esm.getHT(mData, 24); + hasData = true; + break; + case ESM::FourCC<'S','C','R','I'>::value: + mScript = esm.getHString(); + break; + case ESM::FourCC<'S','N','A','M'>::value: + mSound = esm.getHString(); + break; + default: + esm.fail("Unknown subrecord"); + } + } + if (!hasData) + esm.fail("Missing LHDT subrecord"); + } + void Light::save(ESMWriter &esm) const + { + esm.writeHNCString("MODL", mModel); + esm.writeHNOCString("FNAM", mName); + esm.writeHNOCString("ITEX", mIcon); + esm.writeHNT("LHDT", mData, 24); + esm.writeHNOCString("SCRI", mScript); + esm.writeHNOCString("SNAM", mSound); + } void Light::blank() { diff --git a/components/esm/loadligh.hpp b/components/esm/loadligh.hpp index 74eb37197..2c83248f8 100644 --- a/components/esm/loadligh.hpp +++ b/components/esm/loadligh.hpp @@ -25,7 +25,7 @@ struct Light Negative = 0x004, // Negative light - i.e. darkness Flicker = 0x008, Fire = 0x010, - OffDefault = 0x020, // Off by default + OffDefault = 0x020, // Off by default - does not burn while placed in a cell, but can burn when equipped by an NPC FlickerSlow = 0x040, Pulse = 0x080, PulseSlow = 0x100 @@ -37,7 +37,7 @@ struct Light int mValue; int mTime; // Duration int mRadius; - int mColor; // 4-byte rgba value + unsigned int mColor; // 4-byte rgba value int mFlags; }; // Size = 24 bytes diff --git a/components/esm/loadlock.cpp b/components/esm/loadlock.cpp index 42677a22b..2747a6f78 100644 --- a/components/esm/loadlock.cpp +++ b/components/esm/loadlock.cpp @@ -8,26 +8,48 @@ namespace ESM { unsigned int Lockpick::sRecordId = REC_LOCK; -void Lockpick::load(ESMReader &esm) -{ - mModel = esm.getHNString("MODL"); - mName = esm.getHNOString("FNAM"); - - esm.getHNT(mData, "LKDT", 16); - - mScript = esm.getHNOString("SCRI"); - mIcon = esm.getHNOString("ITEX"); -} + void Lockpick::load(ESMReader &esm) + { + bool hasData = true; + while (esm.hasMoreSubs()) + { + esm.getSubName(); + uint32_t name = esm.retSubName().val; + switch (name) + { + case ESM::FourCC<'M','O','D','L'>::value: + mModel = esm.getHString(); + break; + case ESM::FourCC<'F','N','A','M'>::value: + mName = esm.getHString(); + break; + case ESM::FourCC<'L','K','D','T'>::value: + esm.getHT(mData, 16); + hasData = true; + break; + case ESM::FourCC<'S','C','R','I'>::value: + mScript = esm.getHString(); + break; + case ESM::FourCC<'I','T','E','X'>::value: + mIcon = esm.getHString(); + break; + default: + esm.fail("Unknown subrecord"); + } + } + if (!hasData) + esm.fail("Missing LKDT subrecord"); + } -void Lockpick::save(ESMWriter &esm) const -{ - esm.writeHNCString("MODL", mModel); - esm.writeHNOCString("FNAM", mName); + void Lockpick::save(ESMWriter &esm) const + { + esm.writeHNCString("MODL", mModel); + esm.writeHNOCString("FNAM", mName); - esm.writeHNT("LKDT", mData, 16); - esm.writeHNOString("SCRI", mScript); - esm.writeHNOCString("ITEX", mIcon); -} + esm.writeHNT("LKDT", mData, 16); + esm.writeHNOString("SCRI", mScript); + esm.writeHNOCString("ITEX", mIcon); + } void Lockpick::blank() { diff --git a/components/esm/loadltex.cpp b/components/esm/loadltex.cpp index bd28c8488..c3e2d50ff 100644 --- a/components/esm/loadltex.cpp +++ b/components/esm/loadltex.cpp @@ -19,4 +19,10 @@ void LandTexture::save(ESMWriter &esm) const esm.writeHNCString("DATA", mTexture); } +void LandTexture::blank() +{ + mTexture.clear(); + mIndex = -1; +} + } diff --git a/components/esm/loadltex.hpp b/components/esm/loadltex.hpp index 5e84428b2..8b45f8211 100644 --- a/components/esm/loadltex.hpp +++ b/components/esm/loadltex.hpp @@ -32,6 +32,9 @@ struct LandTexture std::string mId, mTexture; int mIndex; + void blank(); + ///< Set record to default state (does not touch the ID). + void load(ESMReader &esm); void save(ESMWriter &esm) const; }; diff --git a/components/esm/loadmgef.cpp b/components/esm/loadmgef.cpp index f60191539..6f859ab3c 100644 --- a/components/esm/loadmgef.cpp +++ b/components/esm/loadmgef.cpp @@ -1,6 +1,7 @@ #include "loadmgef.hpp" #include +#include #include @@ -10,6 +11,157 @@ namespace { + static const char *sIds[ESM::MagicEffect::Length] = + { + "WaterBreathing", + "SwiftSwim", + "WaterWalking", + "Shield", + "FireShield", + "LightningShield", + "FrostShield", + "Burden", + "Feather", + "Jump", + "Levitate", + "SlowFall", + "Lock", + "Open", + "FireDamage", + "ShockDamage", + "FrostDamage", + "DrainAttribute", + "DrainHealth", + "DrainMagicka", + "DrainFatigue", + "DrainSkill", + "DamageAttribute", + "DamageHealth", + "DamageMagicka", + "DamageFatigue", + "DamageSkill", + "Poison", + "WeaknessToFire", + "WeaknessToFrost", + "WeaknessToShock", + "WeaknessToMagicka", + "WeaknessToCommonDisease", + "WeaknessToBlightDisease", + "WeaknessToCorprusDisease", + "WeaknessToPoison", + "WeaknessToNormalWeapons", + "DisintegrateWeapon", + "DisintegrateArmor", + "Invisibility", + "Chameleon", + "Light", + "Sanctuary", + "NightEye", + "Charm", + "Paralyze", + "Silence", + "Blind", + "Sound", + "CalmHumanoid", + "CalmCreature", + "FrenzyHumanoid", + "FrenzyCreature", + "DemoralizeHumanoid", + "DemoralizeCreature", + "RallyHumanoid", + "RallyCreature", + "Dispel", + "Soultrap", + "Telekinesis", + "Mark", + "Recall", + "DivineIntervention", + "AlmsiviIntervention", + "DetectAnimal", + "DetectEnchantment", + "DetectKey", + "SpellAbsorption", + "Reflect", + "CureCommonDisease", + "CureBlightDisease", + "CureCorprusDisease", + "CurePoison", + "CureParalyzation", + "RestoreAttribute", + "RestoreHealth", + "RestoreMagicka", + "RestoreFatigue", + "RestoreSkill", + "FortifyAttribute", + "FortifyHealth", + "FortifyMagicka", + "FortifyFatigue", + "FortifySkill", + "FortifyMaximumMagicka", + "AbsorbAttribute", + "AbsorbHealth", + "AbsorbMagicka", + "AbsorbFatigue", + "AbsorbSkill", + "ResistFire", + "ResistFrost", + "ResistShock", + "ResistMagicka", + "ResistCommonDisease", + "ResistBlightDisease", + "ResistCorprusDisease", + "ResistPoison", + "ResistNormalWeapons", + "ResistParalysis", + "RemoveCurse", + "TurnUndead", + "SummonScamp", + "SummonClannfear", + "SummonDaedroth", + "SummonDremora", + "SummonAncestralGhost", + "SummonSkeletalMinion", + "SummonBonewalker", + "SummonGreaterBonewalker", + "SummonBonelord", + "SummonWingedTwilight", + "SummonHunger", + "SummonGoldenSaint", + "SummonFlameAtronach", + "SummonFrostAtronach", + "SummonStormAtronach", + "FortifyAttack", + "CommandCreature", + "CommandHumanoid", + "BoundDagger", + "BoundLongsword", + "BoundMace", + "BoundBattleAxe", + "BoundSpear", + "BoundLongbow", + "ExtraSpell", + "BoundCuirass", + "BoundHelm", + "BoundBoots", + "BoundShield", + "BoundGloves", + "Corprus", + "Vampirism", + "SummonCenturionSphere", + "SunDamage", + "StuntedMagicka", + + // Tribunal only + "SummonFabricant", + + // Bloodmoon only + "SummonWolf", + "SummonBear", + "SummonBonewolf", + "SummonCreature04", + "SummonCreature05" + }; + const int NumberOfHardcodedFlags = 143; const int HardcodedFlags[NumberOfHardcodedFlags] = { 0x11c8, 0x11c0, 0x11c8, 0x11e0, 0x11e0, 0x11e0, 0x11e0, 0x11d0, @@ -39,26 +191,64 @@ namespace ESM void MagicEffect::load(ESMReader &esm) { - esm.getHNT(mIndex, "INDX"); - - esm.getHNT(mData, "MEDT", 36); - if (mIndex>=0 && mIndex=0 && mIndex::value: + mIcon = esm.getHString(); + break; + case ESM::FourCC<'P','T','E','X'>::value: + mParticle = esm.getHString(); + break; + case ESM::FourCC<'B','S','N','D'>::value: + mBoltSound = esm.getHString(); + break; + case ESM::FourCC<'C','S','N','D'>::value: + mCastSound = esm.getHString(); + break; + case ESM::FourCC<'H','S','N','D'>::value: + mHitSound = esm.getHString(); + break; + case ESM::FourCC<'A','S','N','D'>::value: + mAreaSound = esm.getHString(); + break; + case ESM::FourCC<'C','V','F','X'>::value: + mCasting = esm.getHString(); + break; + case ESM::FourCC<'B','V','F','X'>::value: + mBolt = esm.getHString(); + break; + case ESM::FourCC<'H','V','F','X'>::value: + mHit = esm.getHString(); + break; + case ESM::FourCC<'A','V','F','X'>::value: + mArea = esm.getHString(); + break; + case ESM::FourCC<'D','E','S','C'>::value: + mDescription = esm.getHString(); + break; + default: + esm.fail("Unknown subrecord"); + } + } } void MagicEffect::save(ESMWriter &esm) const { @@ -383,4 +573,51 @@ MagicEffect::MagnitudeDisplayType MagicEffect::getMagnitudeDisplayType() const { return MDT_Points; } + void MagicEffect::blank() + { + mData.mSchool = 0; + mData.mBaseCost = 0; + mData.mFlags = 0; + mData.mRed = 0; + mData.mGreen = 0; + mData.mBlue = 0; + mData.mSpeed = 0; + + mIcon.clear(); + mParticle.clear(); + mCasting.clear(); + mHit.clear(); + mArea.clear(); + mBolt.clear(); + mCastSound.clear(); + mBoltSound.clear(); + mHitSound.clear(); + mAreaSound.clear(); + mDescription.clear(); + } + + std::string MagicEffect::indexToId (int index) + { + std::ostringstream stream; + + if (index!=-1) + { + stream << "#"; + + if (index<100) + { + stream << "0"; + + if (index<10) + stream << "0"; + } + + stream << index; + + if (index>=0 && index sNames; @@ -86,6 +97,8 @@ struct MagicEffect void load(ESMReader &esm); void save(ESMWriter &esm) const; + /// Set record to default state (does not touch the ID/index). + void blank(); enum Effects { @@ -239,6 +252,8 @@ struct MagicEffect Length }; + + static std::string indexToId (int index); }; } #endif diff --git a/components/esm/loadmisc.cpp b/components/esm/loadmisc.cpp index 2ca09e8ae..81c094f2b 100644 --- a/components/esm/loadmisc.cpp +++ b/components/esm/loadmisc.cpp @@ -8,22 +8,45 @@ namespace ESM { unsigned int Miscellaneous::sRecordId = REC_MISC; -void Miscellaneous::load(ESMReader &esm) -{ - mModel = esm.getHNString("MODL"); - mName = esm.getHNOString("FNAM"); - esm.getHNT(mData, "MCDT", 12); - mScript = esm.getHNOString("SCRI"); - mIcon = esm.getHNOString("ITEX"); -} -void Miscellaneous::save(ESMWriter &esm) const -{ - esm.writeHNCString("MODL", mModel); - esm.writeHNOCString("FNAM", mName); - esm.writeHNT("MCDT", mData, 12); - esm.writeHNOCString("SCRI", mScript); - esm.writeHNOCString("ITEX", mIcon); -} + void Miscellaneous::load(ESMReader &esm) + { + bool hasData = false; + while (esm.hasMoreSubs()) + { + esm.getSubName(); + uint32_t name = esm.retSubName().val; + switch (name) + { + case ESM::FourCC<'M','O','D','L'>::value: + mModel = esm.getHString(); + break; + case ESM::FourCC<'F','N','A','M'>::value: + mName = esm.getHString(); + break; + case ESM::FourCC<'M','C','D','T'>::value: + esm.getHT(mData, 12); + hasData = true; + break; + case ESM::FourCC<'S','C','R','I'>::value: + mScript = esm.getHString(); + break; + case ESM::FourCC<'I','T','E','X'>::value: + mIcon = esm.getHString(); + break; + } + } + if (!hasData) + esm.fail("Missing MCDT subrecord"); + } + + void Miscellaneous::save(ESMWriter &esm) const + { + esm.writeHNCString("MODL", mModel); + esm.writeHNOCString("FNAM", mName); + esm.writeHNT("MCDT", mData, 12); + esm.writeHNOCString("SCRI", mScript); + esm.writeHNOCString("ITEX", mIcon); + } void Miscellaneous::blank() { diff --git a/components/esm/loadnpc.cpp b/components/esm/loadnpc.cpp index 2fe9fe3c1..d90b4816d 100644 --- a/components/esm/loadnpc.cpp +++ b/components/esm/loadnpc.cpp @@ -8,91 +8,136 @@ namespace ESM { unsigned int NPC::sRecordId = REC_NPC_; -void NPC::load(ESMReader &esm) -{ - mPersistent = esm.getRecordFlags() & 0x0400; - - mModel = esm.getHNOString("MODL"); - mName = esm.getHNOString("FNAM"); - - mRace = esm.getHNString("RNAM"); - mClass = esm.getHNString("CNAM"); - mFaction = esm.getHNString("ANAM"); - mHead = esm.getHNString("BNAM"); - mHair = esm.getHNString("KNAM"); - - mScript = esm.getHNOString("SCRI"); - - esm.getSubNameIs("NPDT"); - esm.getSubHeader(); - if (esm.getSubSize() == 52) - { - mNpdtType = NPC_DEFAULT; - esm.getExact(&mNpdt52, 52); - } - else if (esm.getSubSize() == 12) + void NPC::load(ESMReader &esm) { - mNpdtType = NPC_WITH_AUTOCALCULATED_STATS; - esm.getExact(&mNpdt12, 12); - } - else - esm.fail("NPC_NPDT must be 12 or 52 bytes long"); + mPersistent = esm.getRecordFlags() & 0x0400; - esm.getHNT(mFlags, "FLAG"); - - mInventory.load(esm); - mSpells.load(esm); + mSpells.mList.clear(); + mInventory.mList.clear(); + mTransport.clear(); + mAiPackage.mList.clear(); - if (esm.isNextSub("AIDT")) - { - esm.getHExact(&mAiData, sizeof(mAiData)); - mHasAI= true; - } - else + bool hasNpdt = false; + bool hasFlags = false; mHasAI = false; - - while (esm.isNextSub("DODT") || esm.isNextSub("DNAM")) { - if (esm.retSubName() == 0x54444f44) { // DODT struct - Dest dodt; - esm.getHExact(&dodt.mPos, 24); - mTransport.push_back(dodt); - } else if (esm.retSubName() == 0x4d414e44) { // DNAM struct - mTransport.back().mCellName = esm.getHString(); + while (esm.hasMoreSubs()) + { + esm.getSubName(); + uint32_t name = esm.retSubName().val; + switch (name) + { + case ESM::FourCC<'M','O','D','L'>::value: + mModel = esm.getHString(); + break; + case ESM::FourCC<'F','N','A','M'>::value: + mName = esm.getHString(); + break; + case ESM::FourCC<'R','N','A','M'>::value: + mRace = esm.getHString(); + break; + case ESM::FourCC<'C','N','A','M'>::value: + mClass = esm.getHString(); + break; + case ESM::FourCC<'A','N','A','M'>::value: + mFaction = esm.getHString(); + break; + case ESM::FourCC<'B','N','A','M'>::value: + mHead = esm.getHString(); + break; + case ESM::FourCC<'K','N','A','M'>::value: + mHair = esm.getHString(); + break; + case ESM::FourCC<'S','C','R','I'>::value: + mScript = esm.getHString(); + break; + case ESM::FourCC<'N','P','D','T'>::value: + hasNpdt = true; + esm.getSubHeader(); + if (esm.getSubSize() == 52) + { + mNpdtType = NPC_DEFAULT; + esm.getExact(&mNpdt52, 52); + } + else if (esm.getSubSize() == 12) + { + mNpdtType = NPC_WITH_AUTOCALCULATED_STATS; + esm.getExact(&mNpdt12, 12); + } + else + esm.fail("NPC_NPDT must be 12 or 52 bytes long"); + break; + case ESM::FourCC<'F','L','A','G'>::value: + hasFlags = true; + esm.getHT(mFlags); + break; + case ESM::FourCC<'N','P','C','S'>::value: + mSpells.add(esm); + break; + case ESM::FourCC<'N','P','C','O'>::value: + mInventory.add(esm); + break; + case ESM::FourCC<'A','I','D','T'>::value: + esm.getHExact(&mAiData, sizeof(mAiData)); + mHasAI= true; + break; + case ESM::FourCC<'D','O','D','T'>::value: + { + Dest dodt; + esm.getHExact(&dodt.mPos, 24); + mTransport.push_back(dodt); + break; + } + case ESM::FourCC<'D','N','A','M'>::value: + mTransport.back().mCellName = esm.getHString(); + break; + case AI_Wander: + case AI_Activate: + case AI_Escort: + case AI_Follow: + case AI_Travel: + case AI_CNDT: + mAiPackage.add(esm); + break; + default: + esm.fail("Unknown subrecord"); + } } + if (!hasNpdt) + esm.fail("Missing NPDT subrecord"); + if (!hasFlags) + esm.fail("Missing FLAG subrecord"); } - mAiPackage.load(esm); -} -void NPC::save(ESMWriter &esm) const -{ - esm.writeHNOCString("MODL", mModel); - esm.writeHNOCString("FNAM", mName); - esm.writeHNCString("RNAM", mRace); - esm.writeHNCString("CNAM", mClass); - esm.writeHNCString("ANAM", mFaction); - esm.writeHNCString("BNAM", mHead); - esm.writeHNCString("KNAM", mHair); - esm.writeHNOCString("SCRI", mScript); - - if (mNpdtType == NPC_DEFAULT) - esm.writeHNT("NPDT", mNpdt52, 52); - else if (mNpdtType == NPC_WITH_AUTOCALCULATED_STATS) - esm.writeHNT("NPDT", mNpdt12, 12); - - esm.writeHNT("FLAG", mFlags); - - mInventory.save(esm); - mSpells.save(esm); - if (mHasAI) { - esm.writeHNT("AIDT", mAiData, sizeof(mAiData)); - } + void NPC::save(ESMWriter &esm) const + { + esm.writeHNOCString("MODL", mModel); + esm.writeHNOCString("FNAM", mName); + esm.writeHNCString("RNAM", mRace); + esm.writeHNCString("CNAM", mClass); + esm.writeHNCString("ANAM", mFaction); + esm.writeHNCString("BNAM", mHead); + esm.writeHNCString("KNAM", mHair); + esm.writeHNOCString("SCRI", mScript); + + if (mNpdtType == NPC_DEFAULT) + esm.writeHNT("NPDT", mNpdt52, 52); + else if (mNpdtType == NPC_WITH_AUTOCALCULATED_STATS) + esm.writeHNT("NPDT", mNpdt12, 12); + + esm.writeHNT("FLAG", mFlags); + + mInventory.save(esm); + mSpells.save(esm); + if (mHasAI) { + esm.writeHNT("AIDT", mAiData, sizeof(mAiData)); + } - typedef std::vector::const_iterator DestIter; - for (DestIter it = mTransport.begin(); it != mTransport.end(); ++it) { - esm.writeHNT("DODT", it->mPos, sizeof(it->mPos)); - esm.writeHNOCString("DNAM", it->mCellName); + typedef std::vector::const_iterator DestIter; + for (DestIter it = mTransport.begin(); it != mTransport.end(); ++it) { + esm.writeHNT("DODT", it->mPos, sizeof(it->mPos)); + esm.writeHNOCString("DNAM", it->mCellName); + } + mAiPackage.save(esm); } - mAiPackage.save(esm); -} bool NPC::isMale() const { return (mFlags & Female) == 0; @@ -143,4 +188,14 @@ void NPC::save(ESMWriter &esm) const mHair.clear(); mHead.clear(); } + + int NPC::getFactionRank() const + { + if (mFaction.empty()) + return -1; + else if (mNpdtType == ESM::NPC::NPC_WITH_AUTOCALCULATED_STATS) + return mNpdt12.mRank; + else // NPC_DEFAULT + return mNpdt52.mRank; + } } diff --git a/components/esm/loadnpc.hpp b/components/esm/loadnpc.hpp index fd3e45bdc..9dc3be513 100644 --- a/components/esm/loadnpc.hpp +++ b/components/esm/loadnpc.hpp @@ -80,10 +80,12 @@ struct NPC mPersonality, mLuck; - char mSkills[Skill::Length]; - char mReputation; + // mSkill can grow up to 200, it must be unsigned + unsigned char mSkills[Skill::Length]; + + char mFactionID; unsigned short mHealth, mMana, mFatigue; - char mDisposition, mFactionID, mRank; + signed char mDisposition, mReputation, mRank; char mUnknown; int mGold; }; // 52 bytes @@ -91,9 +93,10 @@ struct NPC struct NPDTstruct12 { short mLevel; - char mDisposition, mReputation, mRank; + // see above + signed char mDisposition, mReputation, mRank; char mUnknown1, mUnknown2, mUnknown3; - int mGold; // ?? not certain + int mGold; }; // 12 bytes struct Dest @@ -103,10 +106,12 @@ struct NPC }; #pragma pack(pop) - char mNpdtType; + unsigned char mNpdtType; NPDTstruct52 mNpdt52; NPDTstruct12 mNpdt12; //for autocalculated characters + int getFactionRank() const; /// wrapper for mNpdt*, -1 = no rank + int mFlags; bool mPersistent; diff --git a/components/esm/loadnpcc.hpp b/components/esm/loadnpcc.hpp deleted file mode 100644 index c87c2545f..000000000 --- a/components/esm/loadnpcc.hpp +++ /dev/null @@ -1,94 +0,0 @@ -#ifndef OPENMW_ESM_NPCC_H -#define OPENMW_ESM_NPCC_H - -#include - -// TODO: create implementation files to remove this -#include "esmreader.hpp" - -namespace ESM { - -class ESMReader; -class ESMWriter; - -/* - * NPC change information (found in savegame files only). We can't - * read these yet. - * - * Some general observations about savegames: - * - * Magical items/potions/spells/etc are added normally as new ALCH, - * SPEL, etc. records, with unique numeric identifiers. - * - * Books with ability enhancements are listed in the save if they have - * been read. - * - * GLOB records set global variables. - * - * SCPT records do not define new scripts, but assign values to the - * variables of existing ones. - * - * STLN - stolen items, ONAM is the owner - * - * GAME - contains a GMDT (game data) of unknown format - * - * VFXM, SPLM, KLST - no clue - * - * PCDT - seems to contain a lot of DNAMs, strings? - * - * FMAP - MAPH and MAPD, probably map data. - * - * JOUR - the entire journal in html - * - * QUES - seems to contain all the quests in the game, not just the - * ones you have done or begun. - * - * REGN - lists all regions in the game, even unvisited ones. - * - * The DIAL/INFO blocks contain changes to characters' dialog status. - * - * Dammit there's a lot of stuff in there! Should really have - * suspected as much. The strategy further is to completely ignore - * save files for the time being. - * - * Several records have a "change" variant, like NPCC, CNTC - * (contents), and CREC (creature.) These seem to alter specific - * instances of creatures, npcs, etc. I have not identified most of - * their subrecords yet. - * - * Several NPCC records have names that begin with "chargen ", I don't - * know if it means something special yet. - * - * The CNTC blocks seem to be instances of leveled lists. When a - * container is supposed to contain this leveled list of this type, - * but is referenced elsewhere in the file by an INDX, the CNTC with - * the corresponding leveled list identifier and INDX will determine - * the container contents instead. - * - * Some classes of objects seem to be altered, and these include an - * INDX, which is probably an index used by specific references other - * places within the save file. I guess this means 'use this class for - * these objects, not the general class.' All the indices I have - * encountered so far are zero, but they have been for different - * classes (different containers, really) so possibly we start from - * zero for each class. This looks like a mess, but is probably still - * easier than to duplicate everything. I think WRITING this format - * will be harder than reading it. - */ - -struct LoadNPCC -{ - static unsigned int sRecordId; - - std::string mId; - - void load(ESMReader &esm) - { - esm.skipRecord(); - } - void save(ESMWriter &esm) const - { - } -}; -} -#endif diff --git a/components/esm/loadpgrd.cpp b/components/esm/loadpgrd.cpp index efdbdd86b..61f56b511 100644 --- a/components/esm/loadpgrd.cpp +++ b/components/esm/loadpgrd.cpp @@ -8,18 +8,28 @@ namespace ESM { unsigned int Pathgrid::sRecordId = REC_PGRD; - Pathgrid::Point& Pathgrid::Point::operator=(const float rhs[3]) { + Pathgrid::Point& Pathgrid::Point::operator=(const float rhs[3]) + { mX = rhs[0]; mY = rhs[1]; mZ = rhs[2]; + mAutogenerated = 0; + mConnectionNum = 0; + mUnknown = 0; return *this; } - Pathgrid::Point::Point(const float rhs[3]) { + Pathgrid::Point::Point(const float rhs[3]) + : mAutogenerated(0), + mConnectionNum(0), + mUnknown(0) + { mX = rhs[0]; mY = rhs[1]; mZ = rhs[2]; } - Pathgrid::Point::Point():mX(0),mY(0),mZ(0) { + Pathgrid::Point::Point():mX(0),mY(0),mZ(0),mAutogenerated(0), + mConnectionNum(0),mUnknown(0) + { } void Pathgrid::load(ESMReader &esm) @@ -27,6 +37,9 @@ void Pathgrid::load(ESMReader &esm) esm.getHNT(mData, "DATA", 12); mCell = esm.getHNString("NAME"); + mPoints.clear(); + mEdges.clear(); + // keep track of total connections so we can reserve edge vector size int edgeCount = 0; @@ -112,4 +125,14 @@ void Pathgrid::save(ESMWriter &esm) const } } + void Pathgrid::blank() + { + mCell.clear(); + mData.mX = 0; + mData.mY = 0; + mData.mS1 = 0; + mData.mS2 = 0; + mPoints.clear(); + mEdges.clear(); + } } diff --git a/components/esm/loadpgrd.hpp b/components/esm/loadpgrd.hpp index 60a736991..256b86cda 100644 --- a/components/esm/loadpgrd.hpp +++ b/components/esm/loadpgrd.hpp @@ -53,6 +53,8 @@ struct Pathgrid void load(ESMReader &esm); void save(ESMWriter &esm) const; + + void blank(); }; } #endif diff --git a/components/esm/loadprob.cpp b/components/esm/loadprob.cpp index b736bb64b..c5f80c584 100644 --- a/components/esm/loadprob.cpp +++ b/components/esm/loadprob.cpp @@ -8,26 +8,48 @@ namespace ESM { unsigned int Probe::sRecordId = REC_PROB; -void Probe::load(ESMReader &esm) -{ - mModel = esm.getHNString("MODL"); - mName = esm.getHNOString("FNAM"); - - esm.getHNT(mData, "PBDT", 16); - - mScript = esm.getHNOString("SCRI"); - mIcon = esm.getHNOString("ITEX"); -} + void Probe::load(ESMReader &esm) + { + bool hasData = true; + while (esm.hasMoreSubs()) + { + esm.getSubName(); + uint32_t name = esm.retSubName().val; + switch (name) + { + case ESM::FourCC<'M','O','D','L'>::value: + mModel = esm.getHString(); + break; + case ESM::FourCC<'F','N','A','M'>::value: + mName = esm.getHString(); + break; + case ESM::FourCC<'P','B','D','T'>::value: + esm.getHT(mData, 16); + hasData = true; + break; + case ESM::FourCC<'S','C','R','I'>::value: + mScript = esm.getHString(); + break; + case ESM::FourCC<'I','T','E','X'>::value: + mIcon = esm.getHString(); + break; + default: + esm.fail("Unknown subrecord"); + } + } + if (!hasData) + esm.fail("Missing PBDT subrecord"); + } -void Probe::save(ESMWriter &esm) const -{ - esm.writeHNCString("MODL", mModel); - esm.writeHNOCString("FNAM", mName); + void Probe::save(ESMWriter &esm) const + { + esm.writeHNCString("MODL", mModel); + esm.writeHNOCString("FNAM", mName); - esm.writeHNT("PBDT", mData, 16); - esm.writeHNOString("SCRI", mScript); - esm.writeHNOCString("ITEX", mIcon); -} + esm.writeHNT("PBDT", mData, 16); + esm.writeHNOString("SCRI", mScript); + esm.writeHNOCString("ITEX", mIcon); + } void Probe::blank() { diff --git a/components/esm/loadrace.cpp b/components/esm/loadrace.cpp index 17f2e0267..967c0d0d7 100644 --- a/components/esm/loadrace.cpp +++ b/components/esm/loadrace.cpp @@ -20,10 +20,34 @@ namespace ESM void Race::load(ESMReader &esm) { - mName = esm.getHNOString("FNAM"); - esm.getHNT(mData, "RADT", 140); - mPowers.load(esm); - mDescription = esm.getHNOString("DESC"); + mPowers.mList.clear(); + + bool hasData = false; + while (esm.hasMoreSubs()) + { + esm.getSubName(); + uint32_t name = esm.retSubName().val; + switch (name) + { + case ESM::FourCC<'F','N','A','M'>::value: + mName = esm.getHString(); + break; + case ESM::FourCC<'R','A','D','T'>::value: + esm.getHT(mData, 140); + hasData = true; + break; + case ESM::FourCC<'D','E','S','C'>::value: + mDescription = esm.getHString(); + break; + case ESM::FourCC<'N','P','C','S'>::value: + mPowers.add(esm); + break; + default: + esm.fail("Unknown subrecord"); + } + } + if (!hasData) + esm.fail("Missing RADT subrecord"); } void Race::save(ESMWriter &esm) const { diff --git a/components/esm/loadregn.cpp b/components/esm/loadregn.cpp index 0c3ccbffb..1b08b7217 100644 --- a/components/esm/loadregn.cpp +++ b/components/esm/loadregn.cpp @@ -39,6 +39,7 @@ void Region::load(ESMReader &esm) esm.getHNT(mMapColor, "CNAM"); + mSoundList.clear(); while (esm.hasMoreSubs()) { SoundRef sr; diff --git a/components/esm/loadregn.hpp b/components/esm/loadregn.hpp index 1992c951b..c231b6aa0 100644 --- a/components/esm/loadregn.hpp +++ b/components/esm/loadregn.hpp @@ -24,10 +24,11 @@ struct Region #pragma pack(1) struct WEATstruct { - // I guess these are probabilities - char mClear, mCloudy, mFoggy, mOvercast, mRain, mThunder, mAsh, mBlight, + // These are probabilities that add up to 100 + unsigned char mClear, mCloudy, mFoggy, mOvercast, mRain, mThunder, mAsh, mBlight, // Unknown weather, probably snow and something. Only // present in file version 1.3. + // the engine uses mA as "snow" and mB as "blizard" mA, mB; }; // 10 bytes @@ -35,7 +36,7 @@ struct Region struct SoundRef { NAME32 mSound; - char mChance; + unsigned char mChance; }; // 33 bytes #pragma pack(pop) diff --git a/components/esm/loadrepa.cpp b/components/esm/loadrepa.cpp index 4e6cd7794..f90f9e39d 100644 --- a/components/esm/loadrepa.cpp +++ b/components/esm/loadrepa.cpp @@ -10,13 +10,35 @@ namespace ESM void Repair::load(ESMReader &esm) { - mModel = esm.getHNString("MODL"); - mName = esm.getHNOString("FNAM"); - - esm.getHNT(mData, "RIDT", 16); - - mScript = esm.getHNOString("SCRI"); - mIcon = esm.getHNOString("ITEX"); + bool hasData = true; + while (esm.hasMoreSubs()) + { + esm.getSubName(); + uint32_t name = esm.retSubName().val; + switch (name) + { + case ESM::FourCC<'M','O','D','L'>::value: + mModel = esm.getHString(); + break; + case ESM::FourCC<'F','N','A','M'>::value: + mName = esm.getHString(); + break; + case ESM::FourCC<'R','I','D','T'>::value: + esm.getHT(mData, 16); + hasData = true; + break; + case ESM::FourCC<'S','C','R','I'>::value: + mScript = esm.getHString(); + break; + case ESM::FourCC<'I','T','E','X'>::value: + mIcon = esm.getHString(); + break; + default: + esm.fail("Unknown subrecord"); + } + } + if (!hasData) + esm.fail("Missing RIDT subrecord"); } void Repair::save(ESMWriter &esm) const diff --git a/components/esm/loadscpt.cpp b/components/esm/loadscpt.cpp index aee8628bd..0c2bdd42f 100644 --- a/components/esm/loadscpt.cpp +++ b/components/esm/loadscpt.cpp @@ -7,28 +7,21 @@ namespace ESM { -struct SCHD -{ - NAME32 mName; - Script::SCHDstruct mData; -}; - unsigned int Script::sRecordId = REC_SCPT; -void Script::load(ESMReader &esm) -{ - SCHD data; - esm.getHNT(data, "SCHD", 52); - mData = data.mData; - mId = data.mName.toString(); - - // List of local variables - if (esm.isNextSub("SCVR")) + void Script::loadSCVR(ESMReader &esm) { int s = mData.mStringTableSize; std::vector tmp (s); - esm.getHExact (&tmp[0], s); + // not using getHExact, vanilla doesn't seem to mind unused bytes at the end + esm.getSubHeader(); + int left = esm.getSubSize(); + if (left < s) + esm.fail("SCVR string list is smaller than specified"); + esm.getExact(&tmp[0], s); + if (left > s) + esm.skip(left-s); // skip the leftover junk // Set up the list of variable names mVarNames.resize(mData.mNumShorts + mData.mNumLongs + mData.mNumFloats); @@ -38,54 +31,95 @@ void Script::load(ESMReader &esm) char* str = &tmp[0]; for (size_t i = 0; i < mVarNames.size(); i++) { + // Support '\r' terminated strings like vanilla. See Bug #1324. char *termsym = strchr(str, '\r'); if(termsym) *termsym = '\0'; mVarNames[i] = std::string(str); str += mVarNames[i].size() + 1; if (str - &tmp[0] > s) - esm.fail("String table overflow"); + { + // Apparently SCVR subrecord is not used and variable names are + // determined on the fly from the script text. Therefore don't throw + // an exeption, just log an error and continue. + std::stringstream ss; + + ss << "ESM Error: " << "String table overflow"; + ss << "\n File: " << esm.getName(); + ss << "\n Record: " << esm.getContext().recName.toString(); + ss << "\n Subrecord: " << "SCVR"; + ss << "\n Offset: 0x" << std::hex << esm.getFileOffset(); + std::cerr << ss.str() << std::endl; + break; + } + } } - // Script mData - mScriptData.resize(mData.mScriptDataSize); - esm.getHNExact(&mScriptData[0], mScriptData.size(), "SCDT"); + void Script::load(ESMReader &esm) + { + SCHD data; + esm.getHNT(data, "SCHD", 52); + mData = data.mData; + mId = data.mName.toString(); - // Script text - mScriptText = esm.getHNOString("SCTX"); -} -void Script::save(ESMWriter &esm) const -{ - std::string varNameString; - if (!mVarNames.empty()) - for (std::vector::const_iterator it = mVarNames.begin(); it != mVarNames.end(); ++it) - varNameString.append(*it); + mVarNames.clear(); + + while (esm.hasMoreSubs()) + { + esm.getSubName(); + uint32_t name = esm.retSubName().val; + switch (name) + { + case ESM::FourCC<'S','C','V','R'>::value: + // list of local variables + loadSCVR(esm); + break; + case ESM::FourCC<'S','C','D','T'>::value: + // compiled script + mScriptData.resize(mData.mScriptDataSize); + esm.getHExact(&mScriptData[0], mScriptData.size()); + break; + case ESM::FourCC<'S','C','T','X'>::value: + mScriptText = esm.getHString(); + break; + default: + esm.fail("Unknown subrecord"); + } + } + } - SCHD data; - memset(&data, 0, sizeof(data)); + void Script::save(ESMWriter &esm) const + { + std::string varNameString; + if (!mVarNames.empty()) + for (std::vector::const_iterator it = mVarNames.begin(); it != mVarNames.end(); ++it) + varNameString.append(*it); - data.mData = mData; - memcpy(data.mName.name, mId.c_str(), mId.size()); + SCHD data; + memset(&data, 0, sizeof(data)); - esm.writeHNT("SCHD", data, 52); + data.mData = mData; + memcpy(data.mName.name, mId.c_str(), mId.size()); - if (!mVarNames.empty()) - { - esm.startSubRecord("SCVR"); - for (std::vector::const_iterator it = mVarNames.begin(); it != mVarNames.end(); ++it) + esm.writeHNT("SCHD", data, 52); + + if (!mVarNames.empty()) { - esm.writeHCString(*it); + esm.startSubRecord("SCVR"); + for (std::vector::const_iterator it = mVarNames.begin(); it != mVarNames.end(); ++it) + { + esm.writeHCString(*it); + } + esm.endRecord("SCVR"); } - esm.endRecord("SCVR"); - } - esm.startSubRecord("SCDT"); - esm.write(reinterpret_cast(&mScriptData[0]), mData.mScriptDataSize); - esm.endRecord("SCDT"); + esm.startSubRecord("SCDT"); + esm.write(reinterpret_cast(&mScriptData[0]), mData.mScriptDataSize); + esm.endRecord("SCDT"); - esm.writeHNOString("SCTX", mScriptText); -} + esm.writeHNOString("SCTX", mScriptText); + } void Script::blank() { @@ -95,7 +129,11 @@ void Script::save(ESMWriter &esm) const mVarNames.clear(); mScriptData.clear(); - mScriptText = "Begin " + mId + "\n\nEnd " + mId + "\n"; + + if (mId.find ("::")!=std::string::npos) + mScriptText = "Begin \"" + mId + "\"\n\nEnd " + mId + "\n"; + else + mScriptText = "Begin " + mId + "\n\nEnd " + mId + "\n"; } } diff --git a/components/esm/loadscpt.hpp b/components/esm/loadscpt.hpp index d5200d4c1..deb71de6a 100644 --- a/components/esm/loadscpt.hpp +++ b/components/esm/loadscpt.hpp @@ -23,45 +23,39 @@ public: struct SCHDstruct { - /* Script name. - - NOTE: You should handle the name "Main" (case insensitive) with - care. With tribunal, modders got the ability to add 'start - scripts' to their mods, which is a script that is run at - startup and which runs throughout the game (I think.) - - However, before Tribunal, there was only one startup script, - called "Main". If mods wanted to make their own start scripts, - they had to overwrite Main. This is obviously problem if - multiple mods to this at the same time. - - Although most mods have switched to using Trib-style startup - scripts, some legacy mods might still overwrite Main, and this - can cause problems if several mods do it. I think the best - course of action is to NEVER overwrite main, but instead add - each with a separate unique name and add them to the start - script list. But there might be other problems with this - approach though. - */ - - // These describe the sizes we need to allocate for the script - // data. + /// Data from script-precompling in the editor. + /// \warning Do not use them. OpenCS currently does not precompile scripts. int mNumShorts, mNumLongs, mNumFloats, mScriptDataSize, mStringTableSize; + }; + struct SCHD + { + NAME32 mName; + Script::SCHDstruct mData; }; // 52 bytes std::string mId; SCHDstruct mData; - std::vector mVarNames; // Variable names - std::vector mScriptData; // Compiled bytecode - std::string mScriptText; // Uncompiled script + /// Variable names generated by script-precompiling in the editor. + /// \warning Do not use this field. OpenCS currently does not precompile scripts. + std::vector mVarNames; + + /// Bytecode generated from script-precompiling in the editor. + /// \warning Do not use this field. OpenCS currently does not precompile scripts. + std::vector mScriptData; + + /// Script source code + std::string mScriptText; void load(ESMReader &esm); void save(ESMWriter &esm) const; void blank(); ///< Set record to default state (does not touch the ID/index). + +private: + void loadSCVR(ESMReader &esm); }; } #endif diff --git a/components/esm/loadskil.cpp b/components/esm/loadskil.cpp index b6724e938..7883b8a1a 100644 --- a/components/esm/loadskil.cpp +++ b/components/esm/loadskil.cpp @@ -129,23 +129,47 @@ namespace ESM unsigned int Skill::sRecordId = REC_SKIL; -void Skill::load(ESMReader &esm) -{ - esm.getHNT(mIndex, "INDX"); - esm.getHNT(mData, "SKDT", 24); - mDescription = esm.getHNOString("DESC"); + void Skill::load(ESMReader &esm) + { + bool hasIndex = false; + bool hasData = false; + while (esm.hasMoreSubs()) + { + esm.getSubName(); + uint32_t name = esm.retSubName().val; + switch (name) + { + case ESM::FourCC<'I','N','D','X'>::value: + esm.getHT(mIndex); + hasIndex = true; + break; + case ESM::FourCC<'S','K','D','T'>::value: + esm.getHT(mData, 24); + hasData = true; + break; + case ESM::FourCC<'D','E','S','C'>::value: + mDescription = esm.getHString(); + break; + default: + esm.fail("Unknown subrecord"); + } + } + if (!hasIndex) + esm.fail("Missing INDX"); + if (!hasData) + esm.fail("Missing SKDT"); - // create an ID from the index and the name (only used in the editor and likely to change in the - // future) - mId = indexToId (mIndex); -} + // create an ID from the index and the name (only used in the editor and likely to change in the + // future) + mId = indexToId (mIndex); + } -void Skill::save(ESMWriter &esm) const -{ - esm.writeHNT("INDX", mIndex); - esm.writeHNT("SKDT", mData, 24); - esm.writeHNOString("DESC", mDescription); -} + void Skill::save(ESMWriter &esm) const + { + esm.writeHNT("INDX", mIndex); + esm.writeHNT("SKDT", mData, 24); + esm.writeHNOString("DESC", mDescription); + } void Skill::blank() { diff --git a/components/esm/loadsndg.cpp b/components/esm/loadsndg.cpp index 1a8ca6335..5ee6f5245 100644 --- a/components/esm/loadsndg.cpp +++ b/components/esm/loadsndg.cpp @@ -8,18 +8,43 @@ namespace ESM { unsigned int SoundGenerator::sRecordId = REC_SNDG; -void SoundGenerator::load(ESMReader &esm) -{ - esm.getHNT(mType, "DATA", 4); - - mCreature = esm.getHNOString("CNAM"); - mSound = esm.getHNOString("SNAM"); -} -void SoundGenerator::save(ESMWriter &esm) const -{ - esm.writeHNT("DATA", mType, 4); - esm.writeHNOCString("CNAM", mCreature); - esm.writeHNOCString("SNAM", mSound); -} + void SoundGenerator::load(ESMReader &esm) + { + bool hasData = false; + while (esm.hasMoreSubs()) + { + esm.getSubName(); + uint32_t name = esm.retSubName().val; + switch (name) + { + case ESM::FourCC<'D','A','T','A'>::value: + esm.getHT(mType, 4); + hasData = true; + break; + case ESM::FourCC<'C','N','A','M'>::value: + mCreature = esm.getHString(); + break; + case ESM::FourCC<'S','N','A','M'>::value: + mSound = esm.getHString(); + break; + default: + esm.fail("Unknown subrecord"); + } + } + if (!hasData) + esm.fail("Missing DATA"); + } + void SoundGenerator::save(ESMWriter &esm) const + { + esm.writeHNT("DATA", mType, 4); + esm.writeHNOCString("CNAM", mCreature); + esm.writeHNOCString("SNAM", mSound); + } + void SoundGenerator::blank() + { + mType = LeftFoot; + mCreature.clear(); + mSound.clear(); + } } diff --git a/components/esm/loadsndg.hpp b/components/esm/loadsndg.hpp index 5509661c1..f89a11208 100644 --- a/components/esm/loadsndg.hpp +++ b/components/esm/loadsndg.hpp @@ -36,6 +36,8 @@ struct SoundGenerator void load(ESMReader &esm); void save(ESMWriter &esm) const; + + void blank(); }; } #endif diff --git a/components/esm/loadsoun.cpp b/components/esm/loadsoun.cpp index 28e4d7d9c..690c1b448 100644 --- a/components/esm/loadsoun.cpp +++ b/components/esm/loadsoun.cpp @@ -8,22 +8,35 @@ namespace ESM { unsigned int Sound::sRecordId = REC_SOUN; -void Sound::load(ESMReader &esm) -{ - mSound = esm.getHNOString("FNAM"); - esm.getHNT(mData, "DATA", 3); - /* - cout << "vol=" << (int)data.volume - << " min=" << (int)data.minRange - << " max=" << (int)data.maxRange - << endl; - */ -} -void Sound::save(ESMWriter &esm) const -{ - esm.writeHNOCString("FNAM", mSound); - esm.writeHNT("DATA", mData, 3); -} + void Sound::load(ESMReader &esm) + { + bool hasData = false; + while (esm.hasMoreSubs()) + { + esm.getSubName(); + uint32_t name = esm.retSubName().val; + switch (name) + { + case ESM::FourCC<'F','N','A','M'>::value: + mSound = esm.getHString(); + break; + case ESM::FourCC<'D','A','T','A'>::value: + esm.getHT(mData, 3); + hasData = true; + break; + default: + esm.fail("Unknown subrecord"); + } + } + if (!hasData) + esm.fail("Missing DATA"); + } + + void Sound::save(ESMWriter &esm) const + { + esm.writeHNOCString("FNAM", mSound); + esm.writeHNT("DATA", mData, 3); + } void Sound::blank() { diff --git a/components/esm/loadspel.cpp b/components/esm/loadspel.cpp index 2c98d796d..96c048e0a 100644 --- a/components/esm/loadspel.cpp +++ b/components/esm/loadspel.cpp @@ -8,19 +8,41 @@ namespace ESM { unsigned int Spell::sRecordId = REC_SPEL; -void Spell::load(ESMReader &esm) -{ - mName = esm.getHNOString("FNAM"); - esm.getHNT(mData, "SPDT", 12); - mEffects.load(esm); -} + void Spell::load(ESMReader &esm) + { + mEffects.mList.clear(); + bool hasData = false; + while (esm.hasMoreSubs()) + { + esm.getSubName(); + uint32_t val = esm.retSubName().val; -void Spell::save(ESMWriter &esm) const -{ - esm.writeHNOCString("FNAM", mName); - esm.writeHNT("SPDT", mData, 12); - mEffects.save(esm); -} + switch (val) + { + case ESM::FourCC<'F','N','A','M'>::value: + mName = esm.getHString(); + break; + case ESM::FourCC<'S','P','D','T'>::value: + esm.getHT(mData, 12); + hasData = true; + break; + case ESM::FourCC<'E','N','A','M'>::value: + ENAMstruct s; + esm.getHT(s, 24); + mEffects.mList.push_back(s); + break; + } + } + if (!hasData) + esm.fail("Missing SPDT subrecord"); + } + + void Spell::save(ESMWriter &esm) const + { + esm.writeHNOCString("FNAM", mName); + esm.writeHNT("SPDT", mData, 12); + mEffects.save(esm); + } void Spell::blank() { diff --git a/components/esm/loadsscr.cpp b/components/esm/loadsscr.cpp index 69b04bb23..7380dd0a7 100644 --- a/components/esm/loadsscr.cpp +++ b/components/esm/loadsscr.cpp @@ -8,15 +8,41 @@ namespace ESM { unsigned int StartScript::sRecordId = REC_SSCR; -void StartScript::load(ESMReader &esm) -{ - mData = esm.getHNString("DATA"); - mScript = esm.getHNString("NAME"); -} -void StartScript::save(ESMWriter &esm) const -{ - esm.writeHNString("DATA", mData); - esm.writeHNString("NAME", mScript); -} + void StartScript::load(ESMReader &esm) + { + bool hasData = false; + bool hasName = false; + while (esm.hasMoreSubs()) + { + esm.getSubName(); + uint32_t name = esm.retSubName().val; + switch (name) + { + case ESM::FourCC<'D','A','T','A'>::value: + mData = esm.getHString(); + hasData = true; + break; + case ESM::FourCC<'N','A','M','E'>::value: + mId = esm.getHString(); + hasName = true; + break; + default: + esm.fail("Unknown subrecord"); + } + } + if (!hasData) + esm.fail("Missing DATA"); + if (!hasName) + esm.fail("Missing NAME"); + } + void StartScript::save(ESMWriter &esm) const + { + esm.writeHNString("DATA", mData); + esm.writeHNString("NAME", mId); + } + void StartScript::blank() + { + mData.clear(); + } } diff --git a/components/esm/loadsscr.hpp b/components/esm/loadsscr.hpp index d09ad883e..1420d16c4 100644 --- a/components/esm/loadsscr.hpp +++ b/components/esm/loadsscr.hpp @@ -22,11 +22,13 @@ struct StartScript static unsigned int sRecordId; std::string mData; - std::string mId, mScript; + std::string mId; // Load a record and add it to the list void load(ESMReader &esm); void save(ESMWriter &esm) const; + + void blank(); }; } diff --git a/components/esm/loadstat.cpp b/components/esm/loadstat.cpp index 53d1b4bb5..2bb817332 100644 --- a/components/esm/loadstat.cpp +++ b/components/esm/loadstat.cpp @@ -8,15 +8,16 @@ namespace ESM { unsigned int Static::sRecordId = REC_STAT; -void Static::load(ESMReader &esm) -{ - mPersistent = esm.getRecordFlags() & 0x0400; - mModel = esm.getHNString("MODL"); -} -void Static::save(ESMWriter &esm) const -{ - esm.writeHNCString("MODL", mModel); -} + void Static::load(ESMReader &esm) + { + mPersistent = esm.getRecordFlags() & 0x0400; + + mModel = esm.getHNString("MODL"); + } + void Static::save(ESMWriter &esm) const + { + esm.writeHNCString("MODL", mModel); + } void Static::blank() { diff --git a/components/esm/loadtes3.cpp b/components/esm/loadtes3.cpp index d3d525049..9c0c55b8f 100644 --- a/components/esm/loadtes3.cpp +++ b/components/esm/loadtes3.cpp @@ -45,6 +45,25 @@ void ESM::Header::load (ESMReader &esm) m.size = esm.getHNLong ("DATA"); mMaster.push_back (m); } + + if (esm.isNextSub("GMDT")) + { + esm.getHT(mGameData); + } + if (esm.isNextSub("SCRD")) + { + esm.getSubHeader(); + mSCRD.resize(esm.getSubSize()); + if (mSCRD.size()) + esm.getExact(&mSCRD[0], mSCRD.size()); + } + if (esm.isNextSub("SCRS")) + { + esm.getSubHeader(); + mSCRS.resize(esm.getSubSize()); + if (mSCRS.size()) + esm.getExact(&mSCRS[0], mSCRS.size()); + } } void ESM::Header::save (ESMWriter &esm) diff --git a/components/esm/loadtes3.hpp b/components/esm/loadtes3.hpp index eb5e14daf..f2dde4d1f 100644 --- a/components/esm/loadtes3.hpp +++ b/components/esm/loadtes3.hpp @@ -39,6 +39,20 @@ namespace ESM int index; // Position of the parent file in the global list of loaded files }; + struct GMDT + { + float mCurrentHealth; + float mMaximumHealth; + float mHour; + unsigned char unknown1[12]; + NAME64 mCurrentCell; + unsigned char unknown2[4]; + NAME32 mPlayerName; + }; + GMDT mGameData; // Used in .ess savegames only + std::vector mSCRD; // Used in .ess savegames only, unknown + std::vector mSCRS; // Used in .ess savegames only, screenshot + Data mData; int mFormat; std::vector mMaster; diff --git a/components/esm/loadweap.cpp b/components/esm/loadweap.cpp index 1d0b149df..981a5815a 100644 --- a/components/esm/loadweap.cpp +++ b/components/esm/loadweap.cpp @@ -8,24 +8,50 @@ namespace ESM { unsigned int Weapon::sRecordId = REC_WEAP; -void Weapon::load(ESMReader &esm) -{ - mModel = esm.getHNString("MODL"); - mName = esm.getHNOString("FNAM"); - esm.getHNT(mData, "WPDT", 32); - mScript = esm.getHNOString("SCRI"); - mIcon = esm.getHNOString("ITEX"); - mEnchant = esm.getHNOString("ENAM"); -} -void Weapon::save(ESMWriter &esm) const -{ - esm.writeHNCString("MODL", mModel); - esm.writeHNOCString("FNAM", mName); - esm.writeHNT("WPDT", mData, 32); - esm.writeHNOCString("SCRI", mScript); - esm.writeHNOCString("ITEX", mIcon); - esm.writeHNOCString("ENAM", mEnchant); -} + void Weapon::load(ESMReader &esm) + { + bool hasData = false; + while (esm.hasMoreSubs()) + { + esm.getSubName(); + uint32_t name = esm.retSubName().val; + switch (name) + { + case ESM::FourCC<'M','O','D','L'>::value: + mModel = esm.getHString(); + break; + case ESM::FourCC<'F','N','A','M'>::value: + mName = esm.getHString(); + break; + case ESM::FourCC<'W','P','D','T'>::value: + esm.getHT(mData, 32); + hasData = true; + break; + case ESM::FourCC<'S','C','R','I'>::value: + mScript = esm.getHString(); + break; + case ESM::FourCC<'I','T','E','X'>::value: + mIcon = esm.getHString(); + break; + case ESM::FourCC<'E','N','A','M'>::value: + mEnchant = esm.getHString(); + break; + default: + esm.fail("Unknown subrecord"); + } + } + if (!hasData) + esm.fail("Missing WPDT subrecord"); + } + void Weapon::save(ESMWriter &esm) const + { + esm.writeHNCString("MODL", mModel); + esm.writeHNOCString("FNAM", mName); + esm.writeHNT("WPDT", mData, 32); + esm.writeHNOCString("SCRI", mScript); + esm.writeHNOCString("ITEX", mIcon); + esm.writeHNOCString("ENAM", mEnchant); + } void Weapon::blank() { diff --git a/components/esm/locals.cpp b/components/esm/locals.cpp index 9c470a025..f0cfd49f0 100644 --- a/components/esm/locals.cpp +++ b/components/esm/locals.cpp @@ -11,7 +11,7 @@ void ESM::Locals::load (ESMReader &esm) std::string id = esm.getHString(); Variant value; - value.read (esm, Variant::Format_Info); + value.read (esm, Variant::Format_Local); mVariables.push_back (std::make_pair (id, value)); } @@ -23,6 +23,6 @@ void ESM::Locals::save (ESMWriter &esm) const iter!=mVariables.end(); ++iter) { esm.writeHNString ("LOCA", iter->first); - iter->second.write (esm, Variant::Format_Info); + iter->second.write (esm, Variant::Format_Local); } -} \ No newline at end of file +} diff --git a/components/esm/magiceffects.cpp b/components/esm/magiceffects.cpp new file mode 100644 index 000000000..898e7e4b1 --- /dev/null +++ b/components/esm/magiceffects.cpp @@ -0,0 +1,29 @@ +#include "magiceffects.hpp" + +#include "esmwriter.hpp" +#include "esmreader.hpp" + +namespace ESM +{ + +void MagicEffects::save(ESMWriter &esm) const +{ + for (std::map::const_iterator it = mEffects.begin(); it != mEffects.end(); ++it) + { + esm.writeHNT("EFID", it->first); + esm.writeHNT("BASE", it->second); + } +} + +void MagicEffects::load(ESMReader &esm) +{ + while (esm.isNextSub("EFID")) + { + int id, base; + esm.getHT(id); + esm.getHNT(base, "BASE"); + mEffects.insert(std::make_pair(id, base)); + } +} + +} diff --git a/components/esm/magiceffects.hpp b/components/esm/magiceffects.hpp new file mode 100644 index 000000000..2a6052caa --- /dev/null +++ b/components/esm/magiceffects.hpp @@ -0,0 +1,23 @@ +#ifndef COMPONENTS_ESM_MAGICEFFECTS_H +#define COMPONENTS_ESM_MAGICEFFECTS_H + +#include + +namespace ESM +{ + class ESMReader; + class ESMWriter; + + // format 0, saved games only + struct MagicEffects + { + // + std::map mEffects; + + void load (ESMReader &esm); + void save (ESMWriter &esm) const; + }; + +} + +#endif diff --git a/components/esm/npcstate.cpp b/components/esm/npcstate.cpp index e59ec3e26..724d67326 100644 --- a/components/esm/npcstate.cpp +++ b/components/esm/npcstate.cpp @@ -5,20 +5,34 @@ void ESM::NpcState::load (ESMReader &esm) { ObjectState::load (esm); - mInventory.load (esm); + if (mHasCustomState) + { + mInventory.load (esm); - mNpcStats.load (esm); + mNpcStats.load (esm); - mCreatureStats.load (esm); + mCreatureStats.load (esm); + } } void ESM::NpcState::save (ESMWriter &esm, bool inInventory) const { ObjectState::save (esm, inInventory); - mInventory.save (esm); + if (mHasCustomState) + { + mInventory.save (esm); - mNpcStats.save (esm); + mNpcStats.save (esm); - mCreatureStats.save (esm); -} \ No newline at end of file + mCreatureStats.save (esm); + } +} + +void ESM::NpcState::blank() +{ + ObjectState::blank(); + mNpcStats.blank(); + mCreatureStats.blank(); + mHasCustomState = true; +} diff --git a/components/esm/npcstate.hpp b/components/esm/npcstate.hpp index 39858d553..b90cd85e6 100644 --- a/components/esm/npcstate.hpp +++ b/components/esm/npcstate.hpp @@ -16,6 +16,9 @@ namespace ESM NpcStats mNpcStats; CreatureStats mCreatureStats; + /// Initialize to default state + void blank(); + virtual void load (ESMReader &esm); virtual void save (ESMWriter &esm, bool inInventory = false) const; }; diff --git a/components/esm/npcstats.cpp b/components/esm/npcstats.cpp index 3fa954182..cc1d6b3dd 100644 --- a/components/esm/npcstats.cpp +++ b/components/esm/npcstats.cpp @@ -4,7 +4,7 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -ESM::NpcStats::Faction::Faction() : mExpelled (false), mRank (0), mReputation (0) {} +ESM::NpcStats::Faction::Faction() : mExpelled (false), mRank (-1), mReputation (0) {} void ESM::NpcStats::load (ESMReader &esm) { @@ -57,12 +57,13 @@ void ESM::NpcStats::load (ESMReader &esm) mWerewolfKills = 0; esm.getHNOT (mWerewolfKills, "WKIL"); - mProfit = 0; - esm.getHNOT (mProfit, "PROF"); + // No longer used + if (esm.isNextSub("PROF")) + esm.skipHSub(); // int profit // No longer used. Now part of CreatureStats. - float attackStrength = 0; - esm.getHNOT (attackStrength, "ASTR"); + if (esm.isNextSub("ASTR")) + esm.skipHSub(); // attackStrength mLevelProgress = 0; esm.getHNOT (mLevelProgress, "LPRO"); @@ -75,11 +76,13 @@ void ESM::NpcStats::load (ESMReader &esm) mTimeToStartDrowning = 0; esm.getHNOT (mTimeToStartDrowning, "DRTI"); - mLastDrowningHit = 0; - esm.getHNOT (mLastDrowningHit, "DRLH"); + // No longer used + float lastDrowningHit = 0; + esm.getHNOT (lastDrowningHit, "DRLH"); - mLevelHealthBonus = 0; - esm.getHNOT (mLevelHealthBonus, "LVLH"); + // No longer used + float levelHealthBonus = 0; + esm.getHNOT (levelHealthBonus, "LVLH"); mCrimeId = -1; esm.getHNOT (mCrimeId, "CRID"); @@ -98,7 +101,7 @@ void ESM::NpcStats::save (ESMWriter &esm) const esm.writeHNT ("FAEX", expelled); } - if (iter->second.mRank) + if (iter->second.mRank >= 0) esm.writeHNT ("FARA", iter->second.mRank); if (iter->second.mReputation) @@ -130,9 +133,6 @@ void ESM::NpcStats::save (ESMWriter &esm) const if (mWerewolfKills) esm.writeHNT ("WKIL", mWerewolfKills); - if (mProfit) - esm.writeHNT ("PROF", mProfit); - if (mLevelProgress) esm.writeHNT ("LPRO", mLevelProgress); @@ -145,12 +145,20 @@ void ESM::NpcStats::save (ESMWriter &esm) const if (mTimeToStartDrowning) esm.writeHNT ("DRTI", mTimeToStartDrowning); - if (mLastDrowningHit) - esm.writeHNT ("DRLH", mLastDrowningHit); - - if (mLevelHealthBonus) - esm.writeHNT ("LVLH", mLevelHealthBonus); - if (mCrimeId != -1) esm.writeHNT ("CRID", mCrimeId); } + +void ESM::NpcStats::blank() +{ + mIsWerewolf = false; + mDisposition = 0; + mBounty = 0; + mReputation = 0; + mWerewolfKills = 0; + mLevelProgress = 0; + for (int i=0; i<8; ++i) + mSkillIncrease[i] = 0; + mTimeToStartDrowning = 20; + mCrimeId = -1; +} diff --git a/components/esm/npcstats.hpp b/components/esm/npcstats.hpp index ce7c75d2a..0138ab209 100644 --- a/components/esm/npcstats.hpp +++ b/components/esm/npcstats.hpp @@ -34,21 +34,21 @@ namespace ESM StatState mWerewolfAttributes[8]; bool mIsWerewolf; - std::map mFactions; + std::map mFactions; // lower case IDs int mDisposition; Skill mSkills[27]; int mBounty; int mReputation; int mWerewolfKills; - int mProfit; int mLevelProgress; int mSkillIncrease[8]; - std::vector mUsedIds; + std::vector mUsedIds; // lower case IDs float mTimeToStartDrowning; - float mLastDrowningHit; - float mLevelHealthBonus; int mCrimeId; + /// Initialize to default state + void blank(); + void load (ESMReader &esm); void save (ESMWriter &esm) const; }; diff --git a/components/esm/objectstate.cpp b/components/esm/objectstate.cpp index be00f3ef6..9ef1ccf80 100644 --- a/components/esm/objectstate.cpp +++ b/components/esm/objectstate.cpp @@ -6,7 +6,7 @@ void ESM::ObjectState::load (ESMReader &esm) { - mRef.load (esm, true); + mRef.loadData(esm); mHasLocals = 0; esm.getHNOT (mHasLocals, "HLOC"); @@ -23,6 +23,14 @@ void ESM::ObjectState::load (ESMReader &esm) esm.getHNOT (mPosition, "POS_", 24); esm.getHNOT (mLocalRotation, "LROT", 12); + + // obsolete + int unused; + esm.getHNOT(unused, "LTIM"); + + // FIXME: assuming "false" as default would make more sense, but also break compatibility with older save files + mHasCustomState = true; + esm.getHNOT (mHasCustomState, "HCUS"); } void ESM::ObjectState::save (ESMWriter &esm, bool inInventory) const @@ -46,6 +54,24 @@ void ESM::ObjectState::save (ESMWriter &esm, bool inInventory) const esm.writeHNT ("POS_", mPosition, 24); esm.writeHNT ("LROT", mLocalRotation, 12); } + + if (!mHasCustomState) + esm.writeHNT ("HCUS", false); +} + +void ESM::ObjectState::blank() +{ + mRef.blank(); + mHasLocals = 0; + mEnabled = false; + mCount = 1; + for (int i=0;i<3;++i) + { + mPosition.pos[i] = 0; + mPosition.rot[i] = 0; + mLocalRotation[i] = 0; + } + mHasCustomState = true; } -ESM::ObjectState::~ObjectState() {} \ No newline at end of file +ESM::ObjectState::~ObjectState() {} diff --git a/components/esm/objectstate.hpp b/components/esm/objectstate.hpp index 9c9ca5f2e..d1077733a 100644 --- a/components/esm/objectstate.hpp +++ b/components/esm/objectstate.hpp @@ -26,11 +26,22 @@ namespace ESM ESM::Position mPosition; float mLocalRotation[3]; + // Is there any class-specific state following the ObjectState + bool mHasCustomState; + + ObjectState() : mHasCustomState(true) + {} + + /// @note Does not load the CellRef ID, it should already be loaded before calling this method virtual void load (ESMReader &esm); + virtual void save (ESMWriter &esm, bool inInventory = false) const; + /// Initialize to default state + void blank(); + virtual ~ObjectState(); }; } -#endif \ No newline at end of file +#endif diff --git a/components/esm/player.cpp b/components/esm/player.cpp index 52b44c945..70c4b79e2 100644 --- a/components/esm/player.cpp +++ b/components/esm/player.cpp @@ -6,6 +6,7 @@ void ESM::Player::load (ESMReader &esm) { + mObject.mRef.loadId(esm, true); mObject.load (esm); mCellId.load (esm); diff --git a/components/esm/projectilestate.hpp b/components/esm/projectilestate.hpp index 6e36efb5b..51cd5d8c4 100644 --- a/components/esm/projectilestate.hpp +++ b/components/esm/projectilestate.hpp @@ -8,48 +8,13 @@ #include "effectlist.hpp" +#include "util.hpp" + namespace ESM { // format 0, savegames only - struct Quaternion - { - float mValues[4]; - - Quaternion() {} - Quaternion (Ogre::Quaternion q) - { - mValues[0] = q.w; - mValues[1] = q.x; - mValues[2] = q.y; - mValues[3] = q.z; - } - - operator Ogre::Quaternion () const - { - return Ogre::Quaternion(mValues[0], mValues[1], mValues[2], mValues[3]); - } - }; - - struct Vector3 - { - float mValues[3]; - - Vector3() {} - Vector3 (Ogre::Vector3 v) - { - mValues[0] = v.x; - mValues[1] = v.y; - mValues[2] = v.z; - } - - operator Ogre::Vector3 () const - { - return Ogre::Vector3(&mValues[0]); - } - }; - struct BaseProjectileState { std::string mId; diff --git a/components/esm/queststate.hpp b/components/esm/queststate.hpp index 1769336f2..4966cdc90 100644 --- a/components/esm/queststate.hpp +++ b/components/esm/queststate.hpp @@ -12,7 +12,7 @@ namespace ESM struct QuestState { - std::string mTopic; + std::string mTopic; // lower case id int mState; unsigned char mFinished; @@ -21,4 +21,4 @@ namespace ESM }; } -#endif \ No newline at end of file +#endif diff --git a/components/esm/records.hpp b/components/esm/records.hpp index 7a0452eb3..5c183b6f6 100644 --- a/components/esm/records.hpp +++ b/components/esm/records.hpp @@ -14,7 +14,6 @@ #include "loadclot.hpp" #include "loadcont.hpp" #include "loadcrea.hpp" -#include "loadcrec.hpp" #include "loadinfo.hpp" #include "loaddial.hpp" #include "loaddoor.hpp" @@ -33,7 +32,6 @@ #include "loadmgef.hpp" #include "loadmisc.hpp" #include "loadnpc.hpp" -#include "loadnpcc.hpp" #include "loadpgrd.hpp" #include "loadrace.hpp" #include "loadregn.hpp" diff --git a/components/esm/spelllist.cpp b/components/esm/spelllist.cpp index 24d3c3d0a..71c7b340d 100644 --- a/components/esm/spelllist.cpp +++ b/components/esm/spelllist.cpp @@ -5,11 +5,9 @@ namespace ESM { -void SpellList::load(ESMReader &esm) +void SpellList::add(ESMReader &esm) { - while (esm.isNextSub("NPCS")) { - mList.push_back(esm.getHString()); - } + mList.push_back(esm.getHString()); } void SpellList::save(ESMWriter &esm) const @@ -19,4 +17,12 @@ void SpellList::save(ESMWriter &esm) const } } +bool SpellList::exists(const std::string &spell) const +{ + for (std::vector::const_iterator it = mList.begin(); it != mList.end(); ++it) + if (Misc::StringUtils::ciEqual(*it, spell)) + return true; + return false; +} + } diff --git a/components/esm/spelllist.hpp b/components/esm/spelllist.hpp index 934bdda7a..6fb098065 100644 --- a/components/esm/spelllist.hpp +++ b/components/esm/spelllist.hpp @@ -11,12 +11,18 @@ namespace ESM /** A list of references to spells and spell effects. This is shared between the records BSGN, NPC and RACE. + NPCS subrecord. */ struct SpellList { std::vector mList; - void load(ESMReader &esm); + /// Is this spell ID in mList? + bool exists(const std::string& spell) const; + + /// Load one spell, assumes the subrecord name was already read + void add(ESMReader &esm); + void save(ESMWriter &esm) const; }; } diff --git a/components/esm/spellstate.cpp b/components/esm/spellstate.cpp index 2dca2dcec..3ed3329b4 100644 --- a/components/esm/spellstate.cpp +++ b/components/esm/spellstate.cpp @@ -27,6 +27,34 @@ namespace ESM mSpells[id] = random; } + while (esm.isNextSub("PERM")) + { + std::string spellId = esm.getHString(); + + std::vector permEffectList; + while (esm.isNextSub("EFID")) + { + PermanentSpellEffectInfo info; + esm.getHT(info.mId); + esm.getHNT(info.mArg, "ARG_"); + esm.getHNT(info.mMagnitude, "MAGN"); + + permEffectList.push_back(info); + } + mPermanentSpellEffects[spellId] = permEffectList; + } + + while (esm.isNextSub("CORP")) + { + std::string id = esm.getHString(); + + CorprusStats stats; + esm.getHNT(stats.mWorsenings, "WORS"); + esm.getHNT(stats.mNextWorsening, "TIME"); + + mCorprusSpells[id] = stats; + } + while (esm.isNextSub("USED")) { std::string id = esm.getHString(); @@ -53,6 +81,28 @@ namespace ESM } } + for (std::map >::const_iterator it = mPermanentSpellEffects.begin(); it != mPermanentSpellEffects.end(); ++it) + { + esm.writeHNString("PERM", it->first); + + const std::vector & effects = it->second; + for (std::vector::const_iterator effectIt = effects.begin(); effectIt != effects.end(); ++effectIt) + { + esm.writeHNT("EFID", effectIt->mId); + esm.writeHNT("ARG_", effectIt->mArg); + esm.writeHNT("MAGN", effectIt->mMagnitude); + } + } + + for (std::map::const_iterator it = mCorprusSpells.begin(); it != mCorprusSpells.end(); ++it) + { + esm.writeHNString("CORP", it->first); + + const CorprusStats & stats = it->second; + esm.writeHNT("WORS", stats.mWorsenings); + esm.writeHNT("TIME", stats.mNextWorsening); + } + for (std::map::const_iterator it = mUsedPowers.begin(); it != mUsedPowers.end(); ++it) { esm.writeHNString("USED", it->first); diff --git a/components/esm/spellstate.hpp b/components/esm/spellstate.hpp index cb5c0ff0d..028e6a387 100644 --- a/components/esm/spellstate.hpp +++ b/components/esm/spellstate.hpp @@ -2,6 +2,7 @@ #define OPENMW_ESM_SPELLSTATE_H #include +#include #include #include "defs.hpp" @@ -11,11 +12,29 @@ namespace ESM class ESMReader; class ESMWriter; + // NOTE: spell ids must be lower case struct SpellState { + struct CorprusStats + { + int mWorsenings; + TimeStamp mNextWorsening; + }; + + struct PermanentSpellEffectInfo + { + int mId; + int mArg; + float mMagnitude; + }; + typedef std::map > TContainer; TContainer mSpells; + std::map > mPermanentSpellEffects; + + std::map mCorprusSpells; + std::map mUsedPowers; std::string mSelectedSpell; diff --git a/components/esm/statstate.hpp b/components/esm/statstate.hpp index f1a3b4d79..801d0ce82 100644 --- a/components/esm/statstate.hpp +++ b/components/esm/statstate.hpp @@ -15,7 +15,7 @@ namespace ESM T mMod; // Note: can either be the modifier, or the modified value. // A bit inconsistent, but we can't fix this without breaking compatibility. T mCurrent; - T mDamage; + float mDamage; float mProgress; StatState(); @@ -36,8 +36,14 @@ namespace ESM esm.getHNOT (mMod, "STMO"); mCurrent = 0; esm.getHNOT (mCurrent, "STCU"); - mDamage = 0; - esm.getHNOT (mDamage, "STDA"); + + // mDamage was changed to a float; ensure backwards compatibility + T oldDamage = 0; + esm.getHNOT(oldDamage, "STDA"); + mDamage = oldDamage; + + esm.getHNOT (mDamage, "STDF"); + mProgress = 0; esm.getHNOT (mProgress, "STPR"); } @@ -54,7 +60,7 @@ namespace ESM esm.writeHNT ("STCU", mCurrent); if (mDamage) - esm.writeHNT ("STDA", mDamage); + esm.writeHNT ("STDF", mDamage); if (mProgress) esm.writeHNT ("STPR", mProgress); diff --git a/components/esm/stolenitems.cpp b/components/esm/stolenitems.cpp new file mode 100644 index 000000000..c51b0b99b --- /dev/null +++ b/components/esm/stolenitems.cpp @@ -0,0 +1,47 @@ +#include "stolenitems.hpp" + +#include +#include + +namespace ESM +{ + + void StolenItems::write(ESMWriter &esm) const + { + for (StolenItemsMap::const_iterator it = mStolenItems.begin(); it != mStolenItems.end(); ++it) + { + esm.writeHNString("NAME", it->first); + for (std::map, int>::const_iterator ownerIt = it->second.begin(); + ownerIt != it->second.end(); ++ownerIt) + { + if (ownerIt->first.second) + esm.writeHNString("FNAM", ownerIt->first.first); + else + esm.writeHNString("ONAM", ownerIt->first.first); + esm.writeHNT("COUN", ownerIt->second); + } + } + } + + void StolenItems::load(ESMReader &esm) + { + while (esm.isNextSub("NAME")) + { + std::string itemid = esm.getHString(); + + std::map, int> ownerMap; + while (esm.isNextSub("FNAM") || esm.isNextSub("ONAM")) + { + std::string subname = esm.retSubName().toString(); + std::string owner = esm.getHString(); + bool isFaction = (subname == "FNAM"); + int count; + esm.getHNT(count, "COUN"); + ownerMap.insert(std::make_pair(std::make_pair(owner, isFaction), count)); + } + + mStolenItems[itemid] = ownerMap; + } + } + +} diff --git a/components/esm/stolenitems.hpp b/components/esm/stolenitems.hpp new file mode 100644 index 000000000..928fbbf75 --- /dev/null +++ b/components/esm/stolenitems.hpp @@ -0,0 +1,24 @@ +#ifndef OPENMW_COMPONENTS_ESM_STOLENITEMS_H +#define OPENMW_COMPONENTS_ESM_STOLENITEMS_H + +#include +#include + +namespace ESM +{ + class ESMReader; + class ESMWriter; + + // format 0, saved games only + struct StolenItems + { + typedef std::map, int> > StolenItemsMap; + StolenItemsMap mStolenItems; + + void load(ESM::ESMReader& esm); + void write(ESM::ESMWriter& esm) const; + }; + +} + +#endif diff --git a/components/esm/util.hpp b/components/esm/util.hpp new file mode 100644 index 000000000..bb7f3cf7c --- /dev/null +++ b/components/esm/util.hpp @@ -0,0 +1,51 @@ +#ifndef OPENMW_ESM_UTIL_H +#define OPENMW_ESM_UTIL_H + +#include +#include + +namespace ESM +{ + +// format 0, savegames only + +struct Quaternion +{ + float mValues[4]; + + Quaternion() {} + Quaternion (Ogre::Quaternion q) + { + mValues[0] = q.w; + mValues[1] = q.x; + mValues[2] = q.y; + mValues[3] = q.z; + } + + operator Ogre::Quaternion () const + { + return Ogre::Quaternion(mValues[0], mValues[1], mValues[2], mValues[3]); + } +}; + +struct Vector3 +{ + float mValues[3]; + + Vector3() {} + Vector3 (Ogre::Vector3 v) + { + mValues[0] = v.x; + mValues[1] = v.y; + mValues[2] = v.z; + } + + operator Ogre::Vector3 () const + { + return Ogre::Vector3(&mValues[0]); + } +}; + +} + +#endif diff --git a/components/esm/variant.cpp b/components/esm/variant.cpp index 217ec4407..c65eed5e0 100644 --- a/components/esm/variant.cpp +++ b/components/esm/variant.cpp @@ -13,10 +13,35 @@ namespace const uint32_t STRV = ESM::FourCC<'S','T','R','V'>::value; const uint32_t INTV = ESM::FourCC<'I','N','T','V'>::value; const uint32_t FLTV = ESM::FourCC<'F','L','T','V'>::value; + const uint32_t STTV = ESM::FourCC<'S','T','T','V'>::value; } ESM::Variant::Variant() : mType (VT_None), mData (0) {} +ESM::Variant::Variant(const std::string &value) +{ + mData = 0; + mType = VT_None; + setType(VT_String); + setString(value); +} + +ESM::Variant::Variant(int value) +{ + mData = 0; + mType = VT_None; + setType(VT_Long); + setInteger(value); +} + +ESM::Variant::Variant(float value) +{ + mData = 0; + mType = VT_None; + setType(VT_Float); + setFloat(value); +} + ESM::Variant::~Variant() { delete mData; @@ -117,7 +142,23 @@ void ESM::Variant::read (ESMReader& esm, Format format) esm.fail ("invalid subrecord: " + name.toString()); } } - else // info + else if (format == Format_Info) + { + esm.getSubName(); + NAME name = esm.retSubName(); + + if (name==INTV) + { + type = VT_Int; + } + else if (name==FLTV) + { + type = VT_Float; + } + else + esm.fail ("invalid subrecord: " + name.toString()); + } + else if (format == Format_Local) { esm.getSubName(); NAME name = esm.retSubName(); @@ -130,6 +171,10 @@ void ESM::Variant::read (ESMReader& esm, Format format) { type = VT_Float; } + else if (name==STTV) + { + type = VT_Short; + } else esm.fail ("invalid subrecord: " + name.toString()); } @@ -155,6 +200,9 @@ void ESM::Variant::write (ESMWriter& esm, Format format) const if (format==Format_Info) throw std::runtime_error ("can not serialise variant of type none to info format"); + if (format==Format_Local) + throw std::runtime_error ("can not serialise variant of type none to local format"); + // nothing to do here for GMST format } else diff --git a/components/esm/variant.hpp b/components/esm/variant.hpp index 8ba9bb34f..5f179a7bd 100644 --- a/components/esm/variant.hpp +++ b/components/esm/variant.hpp @@ -33,11 +33,16 @@ namespace ESM { Format_Global, Format_Gmst, - Format_Info // also used for local variables in saved game files + Format_Info, + Format_Local // local script variables in save game files }; Variant(); + Variant (const std::string& value); + Variant (int value); + Variant (float value); + ~Variant(); Variant& operator= (const Variant& variant); @@ -83,4 +88,4 @@ namespace ESM bool operator!= (const Variant& left, const Variant& right); } -#endif \ No newline at end of file +#endif diff --git a/components/esm/variantimp.cpp b/components/esm/variantimp.cpp index 1bacdc077..fd068fc19 100644 --- a/components/esm/variantimp.cpp +++ b/components/esm/variantimp.cpp @@ -81,6 +81,9 @@ void ESM::VariantStringData::read (ESMReader& esm, Variant::Format format, VarTy if (format==Variant::Format_Info) esm.fail ("info variables of type string not supported"); + if (format==Variant::Format_Local) + esm.fail ("local variables of type string not supported"); + // GMST mValue = esm.getHString(); } @@ -173,6 +176,21 @@ void ESM::VariantIntegerData::read (ESMReader& esm, Variant::Format format, VarT esm.getHT (mValue); } + else if (format==Variant::Format_Local) + { + if (type==VT_Short) + { + short value; + esm.getHT(value); + mValue = value; + } + else if (type==VT_Int) + { + esm.getHT(mValue); + } + else + esm.fail("unsupported local variable integer type"); + } } void ESM::VariantIntegerData::write (ESMWriter& esm, Variant::Format format, VarType type) const @@ -204,6 +222,15 @@ void ESM::VariantIntegerData::write (ESMWriter& esm, Variant::Format format, Var esm.writeHNT ("INTV", mValue); } + else if (format==Variant::Format_Local) + { + if (type==VT_Short) + esm.writeHNT ("STTV", (short)mValue); + else if (type == VT_Int) + esm.writeHNT ("INTV", mValue); + else + throw std::runtime_error("unsupported local variable integer type"); + } } bool ESM::VariantIntegerData::isEqual (const VariantDataBase& value) const @@ -252,7 +279,7 @@ void ESM::VariantFloatData::read (ESMReader& esm, Variant::Format format, VarTyp { esm.getHNT (mValue, "FLTV"); } - else if (format==Variant::Format_Gmst || format==Variant::Format_Info) + else if (format==Variant::Format_Gmst || format==Variant::Format_Info || format==Variant::Format_Local) { esm.getHT (mValue); } @@ -268,7 +295,7 @@ void ESM::VariantFloatData::write (ESMWriter& esm, Variant::Format format, VarTy esm.writeHNString ("FNAM", "f"); esm.writeHNT ("FLTV", mValue); } - else if (format==Variant::Format_Gmst || format==Variant::Format_Info) + else if (format==Variant::Format_Gmst || format==Variant::Format_Info || format==Variant::Format_Local) { esm.writeHNT ("FLTV", mValue); } @@ -277,4 +304,4 @@ void ESM::VariantFloatData::write (ESMWriter& esm, Variant::Format format, VarTy bool ESM::VariantFloatData::isEqual (const VariantDataBase& value) const { return dynamic_cast (value).mValue==mValue; -} \ No newline at end of file +} diff --git a/components/esmterrain/storage.cpp b/components/esmterrain/storage.cpp new file mode 100644 index 000000000..9c7beebce --- /dev/null +++ b/components/esmterrain/storage.cpp @@ -0,0 +1,530 @@ +#include "storage.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +namespace ESMTerrain +{ + + bool Storage::getMinMaxHeights(float size, const Ogre::Vector2 ¢er, float &min, float &max) + { + assert (size <= 1 && "Storage::getMinMaxHeights, chunk size should be <= 1 cell"); + + /// \todo investigate if min/max heights should be stored at load time in ESM::Land instead + + Ogre::Vector2 origin = center - Ogre::Vector2(size/2.f, size/2.f); + + assert(origin.x == (int) origin.x); + assert(origin.y == (int) origin.y); + + int cellX = origin.x; + int cellY = origin.y; + + const ESM::Land* land = getLand(cellX, cellY); + if (!land || !(land->mDataTypes&ESM::Land::DATA_VHGT)) + return false; + + min = std::numeric_limits::max(); + max = -std::numeric_limits::max(); + for (int row=0; rowmLandData->mHeights[col*ESM::Land::LAND_SIZE+row]; + if (h > max) + max = h; + if (h < min) + min = h; + } + } + return true; + } + + void Storage::fixNormal (Ogre::Vector3& normal, int cellX, int cellY, int col, int row) + { + while (col >= ESM::Land::LAND_SIZE-1) + { + ++cellY; + col -= ESM::Land::LAND_SIZE-1; + } + while (row >= ESM::Land::LAND_SIZE-1) + { + ++cellX; + row -= ESM::Land::LAND_SIZE-1; + } + while (col < 0) + { + --cellY; + col += ESM::Land::LAND_SIZE-1; + } + while (row < 0) + { + --cellX; + row += ESM::Land::LAND_SIZE-1; + } + ESM::Land* land = getLand(cellX, cellY); + if (land && land->mDataTypes&ESM::Land::DATA_VNML) + { + normal.x = land->mLandData->mNormals[col*ESM::Land::LAND_SIZE*3+row*3]; + normal.y = land->mLandData->mNormals[col*ESM::Land::LAND_SIZE*3+row*3+1]; + normal.z = land->mLandData->mNormals[col*ESM::Land::LAND_SIZE*3+row*3+2]; + normal.normalise(); + } + else + normal = Ogre::Vector3(0,0,1); + } + + void Storage::averageNormal(Ogre::Vector3 &normal, int cellX, int cellY, int col, int row) + { + Ogre::Vector3 n1,n2,n3,n4; + fixNormal(n1, cellX, cellY, col+1, row); + fixNormal(n2, cellX, cellY, col-1, row); + fixNormal(n3, cellX, cellY, col, row+1); + fixNormal(n4, cellX, cellY, col, row-1); + normal = (n1+n2+n3+n4); + normal.normalise(); + } + + void Storage::fixColour (Ogre::ColourValue& color, int cellX, int cellY, int col, int row) + { + if (col == ESM::Land::LAND_SIZE-1) + { + ++cellY; + col = 0; + } + if (row == ESM::Land::LAND_SIZE-1) + { + ++cellX; + row = 0; + } + ESM::Land* land = getLand(cellX, cellY); + if (land && land->mDataTypes&ESM::Land::DATA_VCLR) + { + color.r = land->mLandData->mColours[col*ESM::Land::LAND_SIZE*3+row*3] / 255.f; + color.g = land->mLandData->mColours[col*ESM::Land::LAND_SIZE*3+row*3+1] / 255.f; + color.b = land->mLandData->mColours[col*ESM::Land::LAND_SIZE*3+row*3+2] / 255.f; + } + else + { + color.r = 1; + color.g = 1; + color.b = 1; + } + + } + + void Storage::fillVertexBuffers (int lodLevel, float size, const Ogre::Vector2& center, Terrain::Alignment align, + std::vector& positions, + std::vector& normals, + std::vector& colours) + { + // LOD level n means every 2^n-th vertex is kept + size_t increment = 1 << lodLevel; + + Ogre::Vector2 origin = center - Ogre::Vector2(size/2.f, size/2.f); + assert(origin.x == (int) origin.x); + assert(origin.y == (int) origin.y); + + int startX = origin.x; + int startY = origin.y; + + size_t numVerts = size*(ESM::Land::LAND_SIZE-1)/increment + 1; + + colours.resize(numVerts*numVerts*4); + positions.resize(numVerts*numVerts*3); + normals.resize(numVerts*numVerts*3); + + Ogre::Vector3 normal; + Ogre::ColourValue color; + + float vertY = 0; + float vertX = 0; + + float vertY_ = 0; // of current cell corner + for (int cellY = startY; cellY < startY + std::ceil(size); ++cellY) + { + float vertX_ = 0; // of current cell corner + for (int cellX = startX; cellX < startX + std::ceil(size); ++cellX) + { + ESM::Land* land = getLand(cellX, cellY); + if (land && !(land->mDataTypes&ESM::Land::DATA_VHGT)) + land = NULL; + + int rowStart = 0; + int colStart = 0; + // Skip the first row / column unless we're at a chunk edge, + // since this row / column is already contained in a previous cell + if (colStart == 0 && vertY_ != 0) + colStart += increment; + if (rowStart == 0 && vertX_ != 0) + rowStart += increment; + + vertY = vertY_; + for (int col=colStart; colmLandData->mHeights[col*ESM::Land::LAND_SIZE+row]; + else + positions[vertX*numVerts*3 + vertY*3 + 2] = -2048; + + if (land && land->mDataTypes&ESM::Land::DATA_VNML) + { + normal.x = land->mLandData->mNormals[col*ESM::Land::LAND_SIZE*3+row*3]; + normal.y = land->mLandData->mNormals[col*ESM::Land::LAND_SIZE*3+row*3+1]; + normal.z = land->mLandData->mNormals[col*ESM::Land::LAND_SIZE*3+row*3+2]; + normal.normalise(); + } + else + normal = Ogre::Vector3(0,0,1); + + // Normals apparently don't connect seamlessly between cells + if (col == ESM::Land::LAND_SIZE-1 || row == ESM::Land::LAND_SIZE-1) + fixNormal(normal, cellX, cellY, col, row); + + // some corner normals appear to be complete garbage (z < 0) + if ((row == 0 || row == ESM::Land::LAND_SIZE-1) && (col == 0 || col == ESM::Land::LAND_SIZE-1)) + averageNormal(normal, cellX, cellY, col, row); + + assert(normal.z > 0); + + normals[vertX*numVerts*3 + vertY*3] = normal.x; + normals[vertX*numVerts*3 + vertY*3 + 1] = normal.y; + normals[vertX*numVerts*3 + vertY*3 + 2] = normal.z; + + if (land && land->mDataTypes&ESM::Land::DATA_VCLR) + { + color.r = land->mLandData->mColours[col*ESM::Land::LAND_SIZE*3+row*3] / 255.f; + color.g = land->mLandData->mColours[col*ESM::Land::LAND_SIZE*3+row*3+1] / 255.f; + color.b = land->mLandData->mColours[col*ESM::Land::LAND_SIZE*3+row*3+2] / 255.f; + } + else + { + color.r = 1; + color.g = 1; + color.b = 1; + } + + // Unlike normals, colors mostly connect seamlessly between cells, but not always... + if (col == ESM::Land::LAND_SIZE-1 || row == ESM::Land::LAND_SIZE-1) + fixColour(color, cellX, cellY, col, row); + + color.a = 1; + Ogre::uint32 rsColor; + Ogre::Root::getSingleton().getRenderSystem()->convertColourValue(color, &rsColor); + memcpy(&colours[vertX*numVerts*4 + vertY*4], &rsColor, sizeof(Ogre::uint32)); + + ++vertX; + } + ++vertY; + } + vertX_ = vertX; + } + vertY_ = vertY; + + assert(vertX_ == numVerts); // Ensure we covered whole area + } + assert(vertY_ == numVerts); // Ensure we covered whole area + } + + Storage::UniqueTextureId Storage::getVtexIndexAt(int cellX, int cellY, + int x, int y) + { + // For the first/last row/column, we need to get the texture from the neighbour cell + // to get consistent blending at the borders + --x; + if (x < 0) + { + --cellX; + x += ESM::Land::LAND_TEXTURE_SIZE; + } + if (y >= ESM::Land::LAND_TEXTURE_SIZE) // Y appears to be wrapped from the other side because why the hell not? + { + ++cellY; + y -= ESM::Land::LAND_TEXTURE_SIZE; + } + + assert(xmDataTypes&ESM::Land::DATA_VTEX)) + { + int tex = land->mLandData->mTextures[y * ESM::Land::LAND_TEXTURE_SIZE + x]; + if (tex == 0) + return std::make_pair(0,0); // vtex 0 is always the base texture, regardless of plugin + return std::make_pair(tex, land->mPlugin); + } + else + return std::make_pair(0,0); + } + + std::string Storage::getTextureName(UniqueTextureId id) + { + if (id.first == 0) + return "textures\\_land_default.dds"; // Not sure if the default texture really is hardcoded? + + // NB: All vtex ids are +1 compared to the ltex ids + const ESM::LandTexture* ltex = getLandTexture(id.first-1, id.second); + + //TODO this is needed due to MWs messed up texture handling + std::string texture = Misc::ResourceHelpers::correctTexturePath(ltex->mTexture); + + return texture; + } + + void Storage::getBlendmaps (const std::vector& nodes, std::vector& out, bool pack) + { + for (std::vector::const_iterator it = nodes.begin(); it != nodes.end(); ++it) + { + out.push_back(Terrain::LayerCollection()); + out.back().mTarget = *it; + getBlendmapsImpl((*it)->getSize(), (*it)->getCenter(), pack, out.back().mBlendmaps, out.back().mLayers); + } + } + + void Storage::getBlendmaps(float chunkSize, const Ogre::Vector2 &chunkCenter, + bool pack, std::vector &blendmaps, std::vector &layerList) + { + getBlendmapsImpl(chunkSize, chunkCenter, pack, blendmaps, layerList); + } + + void Storage::getBlendmapsImpl(float chunkSize, const Ogre::Vector2 &chunkCenter, + bool pack, std::vector &blendmaps, std::vector &layerList) + { + // TODO - blending isn't completely right yet; the blending radius appears to be + // different at a cell transition (2 vertices, not 4), so we may need to create a larger blendmap + // and interpolate the rest of the cell by hand? :/ + + Ogre::Vector2 origin = chunkCenter - Ogre::Vector2(chunkSize/2.f, chunkSize/2.f); + int cellX = origin.x; + int cellY = origin.y; + + // Save the used texture indices so we know the total number of textures + // and number of required blend maps + std::set textureIndices; + // Due to the way the blending works, the base layer will always shine through in between + // blend transitions (eg halfway between two texels, both blend values will be 0.5, so 25% of base layer visible). + // To get a consistent look, we need to make sure to use the same base layer in all cells. + // So we're always adding _land_default.dds as the base layer here, even if it's not referenced in this cell. + textureIndices.insert(std::make_pair(0,0)); + + for (int y=0; y textureIndicesMap; + for (std::set::iterator it = textureIndices.begin(); it != textureIndices.end(); ++it) + { + int size = textureIndicesMap.size(); + textureIndicesMap[*it] = size; + layerList.push_back(getLayerInfo(getTextureName(*it))); + } + + int numTextures = textureIndices.size(); + // numTextures-1 since the base layer doesn't need blending + int numBlendmaps = pack ? std::ceil((numTextures-1) / 4.f) : (numTextures-1); + + int channels = pack ? 4 : 1; + + // Second iteration - create and fill in the blend maps + const int blendmapSize = ESM::Land::LAND_TEXTURE_SIZE+1; + + for (int i=0; isecond; + int blendIndex = (pack ? std::floor((layerIndex-1)/4.f) : layerIndex-1); + int channel = pack ? std::max(0, (layerIndex-1) % 4) : 0; + + if (blendIndex == i) + pData[y*blendmapSize*channels + x*channels + channel] = 255; + else + pData[y*blendmapSize*channels + x*channels + channel] = 0; + } + } + blendmaps.push_back(Ogre::PixelBox(blendmapSize, blendmapSize, 1, format, pData)); + } + } + + float Storage::getHeightAt(const Ogre::Vector3 &worldPos) + { + int cellX = std::floor(worldPos.x / 8192.f); + int cellY = std::floor(worldPos.y / 8192.f); + + ESM::Land* land = getLand(cellX, cellY); + if (!land || !(land->mDataTypes&ESM::Land::DATA_VHGT)) + return -2048; + + // Mostly lifted from Ogre::Terrain::getHeightAtTerrainPosition + + // Normalized position in the cell + float nX = (worldPos.x - (cellX * 8192))/8192.f; + float nY = (worldPos.y - (cellY * 8192))/8192.f; + + // get left / bottom points (rounded down) + float factor = ESM::Land::LAND_SIZE - 1.0f; + float invFactor = 1.0f / factor; + + int startX = static_cast(nX * factor); + int startY = static_cast(nY * factor); + int endX = startX + 1; + int endY = startY + 1; + + endX = std::min(endX, ESM::Land::LAND_SIZE-1); + endY = std::min(endY, ESM::Land::LAND_SIZE-1); + + // now get points in terrain space (effectively rounding them to boundaries) + float startXTS = startX * invFactor; + float startYTS = startY * invFactor; + float endXTS = endX * invFactor; + float endYTS = endY * invFactor; + + // get parametric from start coord to next point + float xParam = (nX - startXTS) * factor; + float yParam = (nY - startYTS) * factor; + + /* For even / odd tri strip rows, triangles are this shape: + even odd + 3---2 3---2 + | / | | \ | + 0---1 0---1 + */ + + // Build all 4 positions in normalized cell space, using point-sampled height + Ogre::Vector3 v0 (startXTS, startYTS, getVertexHeight(land, startX, startY) / 8192.f); + Ogre::Vector3 v1 (endXTS, startYTS, getVertexHeight(land, endX, startY) / 8192.f); + Ogre::Vector3 v2 (endXTS, endYTS, getVertexHeight(land, endX, endY) / 8192.f); + Ogre::Vector3 v3 (startXTS, endYTS, getVertexHeight(land, startX, endY) / 8192.f); + // define this plane in terrain space + Ogre::Plane plane; + // (At the moment, all rows have the same triangle alignment) + if (true) + { + // odd row + bool secondTri = ((1.0 - yParam) > xParam); + if (secondTri) + plane.redefine(v0, v1, v3); + else + plane.redefine(v1, v2, v3); + } + else + { + // even row + bool secondTri = (yParam > xParam); + if (secondTri) + plane.redefine(v0, v2, v3); + else + plane.redefine(v0, v1, v2); + } + + // Solve plane equation for z + return (-plane.normal.x * nX + -plane.normal.y * nY + - plane.d) / plane.normal.z * 8192; + + } + + float Storage::getVertexHeight(const ESM::Land *land, int x, int y) + { + assert(x < ESM::Land::LAND_SIZE); + assert(y < ESM::Land::LAND_SIZE); + return land->mLandData->mHeights[y * ESM::Land::LAND_SIZE + x]; + } + + Terrain::LayerInfo Storage::getLayerInfo(const std::string& texture) + { + // Already have this cached? + std::map::iterator found = mLayerInfoMap.find(texture); + if (found != mLayerInfoMap.end()) + return found->second; + + Terrain::LayerInfo info; + info.mParallax = false; + info.mSpecular = false; + info.mDiffuseMap = texture; + std::string texture_ = texture; + boost::replace_last(texture_, ".", "_nh."); + + if (Ogre::ResourceGroupManager::getSingleton().resourceExistsInAnyGroup(texture_)) + { + info.mNormalMap = texture_; + info.mParallax = true; + } + else + { + texture_ = texture; + boost::replace_last(texture_, ".", "_n."); + if (Ogre::ResourceGroupManager::getSingleton().resourceExistsInAnyGroup(texture_)) + info.mNormalMap = texture_; + } + + texture_ = texture; + boost::replace_last(texture_, ".", "_diffusespec."); + if (Ogre::ResourceGroupManager::getSingleton().resourceExistsInAnyGroup(texture_)) + { + info.mDiffuseMap = texture_; + info.mSpecular = true; + } + + // This wasn't cached, so the textures are probably not loaded either. + // Background load them so they are hopefully already loaded once we need them! + Ogre::ResourceBackgroundQueue::getSingleton().load("Texture", info.mDiffuseMap, "General"); + if (!info.mNormalMap.empty()) + Ogre::ResourceBackgroundQueue::getSingleton().load("Texture", info.mNormalMap, "General"); + + mLayerInfoMap[texture] = info; + + return info; + } + + Terrain::LayerInfo Storage::getDefaultLayer() + { + Terrain::LayerInfo info; + info.mDiffuseMap = "textures\\_land_default.dds"; + info.mParallax = false; + info.mSpecular = false; + return info; + } + + float Storage::getCellWorldSize() + { + return ESM::Land::REAL_SIZE; + } + + int Storage::getCellVertices() + { + return ESM::Land::LAND_SIZE; + } + +} diff --git a/components/esmterrain/storage.hpp b/components/esmterrain/storage.hpp new file mode 100644 index 000000000..e184cbc4c --- /dev/null +++ b/components/esmterrain/storage.hpp @@ -0,0 +1,119 @@ +#ifndef COMPONENTS_ESM_TERRAIN_STORAGE_H +#define COMPONENTS_ESM_TERRAIN_STORAGE_H + +#include + +#include +#include + +namespace ESMTerrain +{ + + /// @brief Feeds data from ESM terrain records (ESM::Land, ESM::LandTexture) + /// into the terrain component, converting it on the fly as needed. + class Storage : public Terrain::Storage + { + private: + + // Not implemented in this class, because we need different Store implementations for game and editor + virtual ESM::Land* getLand (int cellX, int cellY) = 0; + virtual const ESM::LandTexture* getLandTexture(int index, short plugin) = 0; + + public: + + // Not implemented in this class, because we need different Store implementations for game and editor + /// Get bounds of the whole terrain in cell units + virtual void getBounds(float& minX, float& maxX, float& minY, float& maxY) = 0; + + /// Get the minimum and maximum heights of a terrain region. + /// @note Will only be called for chunks with size = minBatchSize, i.e. leafs of the quad tree. + /// Larger chunks can simply merge AABB of children. + /// @param size size of the chunk in cell units + /// @param center center of the chunk in cell units + /// @param min min height will be stored here + /// @param max max height will be stored here + /// @return true if there was data available for this terrain chunk + virtual bool getMinMaxHeights (float size, const Ogre::Vector2& center, float& min, float& max); + + /// Fill vertex buffers for a terrain chunk. + /// @note May be called from background threads. Make sure to only call thread-safe functions from here! + /// @note returned colors need to be in render-system specific format! Use RenderSystem::convertColourValue. + /// @note Vertices should be written in row-major order (a row is defined as parallel to the x-axis). + /// The specified positions should be in local space, i.e. relative to the center of the terrain chunk. + /// @param lodLevel LOD level, 0 = most detailed + /// @param size size of the terrain chunk in cell units + /// @param center center of the chunk in cell units + /// @param positions buffer to write vertices + /// @param normals buffer to write vertex normals + /// @param colours buffer to write vertex colours + virtual void fillVertexBuffers (int lodLevel, float size, const Ogre::Vector2& center, Terrain::Alignment align, + std::vector& positions, + std::vector& normals, + std::vector& colours); + + /// Create textures holding layer blend values for a terrain chunk. + /// @note The terrain chunk shouldn't be larger than one cell since otherwise we might + /// have to do a ridiculous amount of different layers. For larger chunks, composite maps should be used. + /// @note May be called from background threads. + /// @param chunkSize size of the terrain chunk in cell units + /// @param chunkCenter center of the chunk in cell units + /// @param pack Whether to pack blend values for up to 4 layers into one texture (one in each channel) - + /// otherwise, each texture contains blend values for one layer only. Shader-based rendering + /// can utilize packing, FFP can't. + /// @param blendmaps created blendmaps will be written here + /// @param layerList names of the layer textures used will be written here + virtual void getBlendmaps (float chunkSize, const Ogre::Vector2& chunkCenter, bool pack, + std::vector& blendmaps, + std::vector& layerList); + + /// Retrieve pixel data for textures holding layer blend values for terrain chunks and layer texture information. + /// This variant is provided to eliminate the overhead of virtual function calls when retrieving a large number of blendmaps at once. + /// @note The terrain chunks shouldn't be larger than one cell since otherwise we might + /// have to do a ridiculous amount of different layers. For larger chunks, composite maps should be used. + /// @note May be called from background threads. + /// @param nodes A collection of nodes for which to retrieve the aforementioned data + /// @param out Output vector + /// @param pack Whether to pack blend values for up to 4 layers into one texture (one in each channel) - + /// otherwise, each texture contains blend values for one layer only. Shader-based rendering + /// can utilize packing, FFP can't. + virtual void getBlendmaps (const std::vector& nodes, std::vector& out, bool pack); + + virtual float getHeightAt (const Ogre::Vector3& worldPos); + + virtual Terrain::LayerInfo getDefaultLayer(); + + /// Get the transformation factor for mapping cell units to world units. + virtual float getCellWorldSize(); + + /// Get the number of vertices on one side for each cell. Should be (power of two)+1 + virtual int getCellVertices(); + + private: + void fixNormal (Ogre::Vector3& normal, int cellX, int cellY, int col, int row); + void fixColour (Ogre::ColourValue& colour, int cellX, int cellY, int col, int row); + void averageNormal (Ogre::Vector3& normal, int cellX, int cellY, int col, int row); + + float getVertexHeight (const ESM::Land* land, int x, int y); + + // Since plugins can define new texture palettes, we need to know the plugin index too + // in order to retrieve the correct texture name. + // pair + typedef std::pair UniqueTextureId; + + UniqueTextureId getVtexIndexAt(int cellX, int cellY, + int x, int y); + std::string getTextureName (UniqueTextureId id); + + std::map mLayerInfoMap; + + Terrain::LayerInfo getLayerInfo(const std::string& texture); + + // Non-virtual + void getBlendmapsImpl (float chunkSize, const Ogre::Vector2& chunkCenter, bool pack, + std::vector& blendmaps, + std::vector& layerList); + }; + +} + +#endif diff --git a/components/files/androidpath.cpp b/components/files/androidpath.cpp new file mode 100644 index 000000000..009bd98a2 --- /dev/null +++ b/components/files/androidpath.cpp @@ -0,0 +1,130 @@ +#include "androidpath.hpp" + +#if defined(__ANDROID__) + +#include +#include +#include +#include "androidpath.h" +#include +#include + + +class Buffer { + public: + static void setData(char const *data); + static char const * getData(); +}; +static char const *path; + +void Buffer::setData(char const *data) +{ + path=data; +} +char const * Buffer::getData() +{ + return path; +} + + +JNIEXPORT void JNICALL Java_ui_activity_GameActivity_getPathToJni(JNIEnv *env, jobject obj, jstring prompt) +{ + jboolean iscopy; + Buffer::setData((env)->GetStringUTFChars(prompt, &iscopy)); +} + +namespace +{ + + boost::filesystem::path getUserHome() + { + const char* dir = getenv("HOME"); + if (dir == NULL) + { + struct passwd* pwd = getpwuid(getuid()); + if (pwd != NULL) + { + dir = pwd->pw_dir; + } + } + if (dir == NULL) + return boost::filesystem::path(); + else + return boost::filesystem::path(dir); + } + + boost::filesystem::path getEnv(const std::string& envVariable, const boost::filesystem::path& fallback) + { + const char* result = getenv(envVariable.c_str()); + if (!result) + return fallback; + boost::filesystem::path dir(result); + if (dir.empty()) + return fallback; + else + return dir; + } +} + +/** + * \namespace Files + */ +namespace Files +{ + +AndroidPath::AndroidPath(const std::string& application_name) + : mName(application_name) +{ +} + +boost::filesystem::path AndroidPath::getUserConfigPath() const +{ + std::string buffer = ""; + buffer = buffer + Buffer::getData() +"/config"; + return getEnv("XDG_CONFIG_HOME", buffer) / mName; +} + +boost::filesystem::path AndroidPath::getUserDataPath() const +{ + std::string buffer = ""; + buffer = buffer + Buffer::getData() +"/share"; + return getEnv("XDG_DATA_HOME", buffer) / mName; +} + +boost::filesystem::path AndroidPath::getCachePath() const +{ + std::string buffer = ""; + buffer = buffer + Buffer::getData() +"/cache"; + return getEnv("XDG_CACHE_HOME", buffer) / mName; +} + +boost::filesystem::path AndroidPath::getGlobalConfigPath() const +{ + std::string buffer = ""; + buffer = buffer + Buffer::getData() +"/"; + boost::filesystem::path globalPath(buffer); + return globalPath / mName; +} + +boost::filesystem::path AndroidPath::getLocalPath() const +{ + return boost::filesystem::path("./"); +} + +boost::filesystem::path AndroidPath::getGlobalDataPath() const +{ + std::string buffer = ""; + buffer = buffer + Buffer::getData() +"/data"; + boost::filesystem::path globalDataPath(buffer); + return globalDataPath / mName; +} + +boost::filesystem::path AndroidPath::getInstallPath() const +{ + return boost::filesystem::path(); +} + + +} /* namespace Files */ + +#endif /* defined(__Android__) */ diff --git a/components/files/androidpath.h b/components/files/androidpath.h new file mode 100644 index 000000000..a93a160e0 --- /dev/null +++ b/components/files/androidpath.h @@ -0,0 +1,19 @@ +/* DO NOT EDIT THIS FILE - it is machine generated */ +#include + +#ifndef _Included_ui_activity_GameActivity_getPathToJni +#define _Included_ui_activity_GameActivity_getPathToJni +#ifdef __cplusplus +extern "C" { +#endif +/* + * Class: Java_org_libsdl_app_SDLActivity_getPathToJni + * Method: getPathToJni + * Signature: (I)I + */ +JNIEXPORT void JNICALL Java_ui_activity_GameActivity_getPathToJni(JNIEnv *env, jobject obj, jstring prompt); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/components/files/androidpath.hpp b/components/files/androidpath.hpp new file mode 100644 index 000000000..a8124e6db --- /dev/null +++ b/components/files/androidpath.hpp @@ -0,0 +1,57 @@ +#ifndef COMPONENTS_FILES_ANDROIDPATH_H +#define COMPONENTS_FILES_ANDROIDPATH_H + +#if defined(__ANDROID__) + +#include +/** + * \namespace Files + */ + + +namespace Files +{ + +struct AndroidPath +{ + AndroidPath(const std::string& application_name); + + + /** + * \brief Return path to the user directory. + */ + boost::filesystem::path getUserConfigPath() const; + + boost::filesystem::path getUserDataPath() const; + + /** + * \brief Return path to the global (system) directory where config files can be placed. + */ + boost::filesystem::path getGlobalConfigPath() const; + + /** + * \brief Return path to the runtime configuration directory which is the + * place where an application was started. + */ + boost::filesystem::path getLocalPath() const; + + /** + * \brief Return path to the global (system) directory where game files can be placed. + */ + boost::filesystem::path getGlobalDataPath() const; + + /** + * \brief + */ + boost::filesystem::path getCachePath() const; + + boost::filesystem::path getInstallPath() const; + + std::string mName; +}; + +} /* namespace Files */ + +#endif /* defined(__Android__) */ + +#endif /* COMPONENTS_FILES_ANDROIDPATH_H */ diff --git a/components/files/configurationmanager.cpp b/components/files/configurationmanager.cpp index 58d75d1fd..e321b5814 100644 --- a/components/files/configurationmanager.cpp +++ b/components/files/configurationmanager.cpp @@ -16,13 +16,20 @@ namespace Files static const char* const openmwCfgFile = "openmw.cfg"; +#if defined(_WIN32) || defined(__WINDOWS__) +static const char* const applicationName = "OpenMW"; +#else +static const char* const applicationName = "openmw"; +#endif + const char* const mwToken = "?mw?"; const char* const localToken = "?local?"; const char* const userDataToken = "?userdata?"; const char* const globalToken = "?global?"; -ConfigurationManager::ConfigurationManager() - : mFixedPath("openmw") +ConfigurationManager::ConfigurationManager(bool silent) + : mFixedPath(applicationName) + , mSilent(silent) { setupTokensMapping(); @@ -123,7 +130,8 @@ void ConfigurationManager::loadConfig(const boost::filesystem::path& path, cfgFile /= std::string(openmwCfgFile); if (boost::filesystem::is_regular_file(cfgFile)) { - std::cout << "Loading config file: " << cfgFile.string() << "... "; + if (!mSilent) + std::cout << "Loading config file: " << cfgFile.string() << "... "; boost::filesystem::ifstream configFileStream(cfgFile); if (configFileStream.is_open()) @@ -131,11 +139,13 @@ void ConfigurationManager::loadConfig(const boost::filesystem::path& path, boost::program_options::store(boost::program_options::parse_config_file( configFileStream, description, true), variables); - std::cout << "done." << std::endl; + if (!mSilent) + std::cout << "done." << std::endl; } else { - std::cout << "failed." << std::endl; + if (!mSilent) + std::cout << "failed." << std::endl; } } } diff --git a/components/files/configurationmanager.hpp b/components/files/configurationmanager.hpp index 35144fe04..b0b7fea9a 100644 --- a/components/files/configurationmanager.hpp +++ b/components/files/configurationmanager.hpp @@ -25,7 +25,7 @@ namespace Files */ struct ConfigurationManager { - ConfigurationManager(); + ConfigurationManager(bool silent=false); /// @param silent Emit log messages to cout? virtual ~ConfigurationManager(); void readConfiguration(boost::program_options::variables_map& variables, @@ -69,6 +69,8 @@ struct ConfigurationManager boost::filesystem::path mLogPath; TokensMappingContainer mTokensMapping; + + bool mSilent; }; } /* namespace Cfg */ diff --git a/components/files/constrainedfiledatastream.cpp b/components/files/constrainedfiledatastream.cpp index 66f6fde97..d4d7c231a 100644 --- a/components/files/constrainedfiledatastream.cpp +++ b/components/files/constrainedfiledatastream.cpp @@ -11,162 +11,172 @@ namespace { class ConstrainedDataStream : public Ogre::DataStream { public: - static const size_t sBufferSize = 4096; // somewhat arbitrary though 64KB buffers didn't seem to improve performance any - static const size_t sBufferThreshold = 1024; // reads larger than this bypass buffering as cost of memcpy outweighs cost of system call + static const size_t sBufferSize = 4096; // somewhat arbitrary though 64KB buffers didn't seem to improve performance any + static const size_t sBufferThreshold = 1024; // reads larger than this bypass buffering as cost of memcpy outweighs cost of system call ConstrainedDataStream(const Ogre::String &fname, size_t start, size_t length) - { - mFile.open (fname.c_str ()); - mSize = length != 0xFFFFFFFF ? length : mFile.size () - start; + : Ogre::DataStream(fname) + { + mFile.open (fname.c_str ()); + mSize = length != 0xFFFFFFFF ? length : mFile.size () - start; - mPos = 0; - mOrigin = start; - mExtent = start + mSize; + mPos = 0; + mOrigin = start; + mExtent = start + mSize; - mBufferOrigin = 0; - mBufferExtent = 0; - } + mBufferOrigin = 0; + mBufferExtent = 0; + } - size_t read(void* buf, size_t count) - { - assert (mPos <= mSize); + size_t read(void* buf, size_t count) + { + try + { + assert (mPos <= mSize); - uint8_t * out = reinterpret_cast (buf); + uint8_t * out = reinterpret_cast (buf); - size_t posBeg = mOrigin + mPos; - size_t posEnd = posBeg + count; + size_t posBeg = mOrigin + mPos; + size_t posEnd = posBeg + count; - if (posEnd > mExtent) - posEnd = mExtent; + if (posEnd > mExtent) + posEnd = mExtent; - size_t posCur = posBeg; + size_t posCur = posBeg; - while (posCur != posEnd) - { - size_t readLeft = posEnd - posCur; + while (posCur != posEnd) + { + size_t readLeft = posEnd - posCur; - if (posCur < mBufferOrigin || posCur >= mBufferExtent) - { - if (readLeft >= sBufferThreshold || (posCur == mOrigin && posEnd == mExtent)) - { - assert (mFile.tell () == mBufferExtent); + if (posCur < mBufferOrigin || posCur >= mBufferExtent) + { + if (readLeft >= sBufferThreshold || (posCur == mOrigin && posEnd == mExtent)) + { + assert (mFile.tell () == mBufferExtent); - if (posCur != mBufferExtent) - mFile.seek (posCur); + if (posCur != mBufferExtent) + mFile.seek (posCur); - posCur += mFile.read (out, readLeft); + posCur += mFile.read (out, readLeft); - mBufferOrigin = mBufferExtent = posCur; + mBufferOrigin = mBufferExtent = posCur; - mPos = posCur - mOrigin; + mPos = posCur - mOrigin; - return posCur - posBeg; - } - else - { - size_t newBufferOrigin; + return posCur - posBeg; + } + else + { + size_t newBufferOrigin; - if ((posCur < mBufferOrigin) && (mBufferOrigin - posCur < sBufferSize)) - newBufferOrigin = std::max (mOrigin, mBufferOrigin > sBufferSize ? mBufferOrigin - sBufferSize : 0); - else - newBufferOrigin = posCur; + if ((posCur < mBufferOrigin) && (mBufferOrigin - posCur < sBufferSize)) + newBufferOrigin = std::max (mOrigin, mBufferOrigin > sBufferSize ? mBufferOrigin - sBufferSize : 0); + else + newBufferOrigin = posCur; - fill (newBufferOrigin); - } - } + fill (newBufferOrigin); + } + } - size_t xfer = std::min (readLeft, mBufferExtent - posCur); + size_t xfer = std::min (readLeft, mBufferExtent - posCur); - memcpy (out, mBuffer + (posCur - mBufferOrigin), xfer); + memcpy (out, mBuffer + (posCur - mBufferOrigin), xfer); - posCur += xfer; - out += xfer; - } + posCur += xfer; + out += xfer; + } - count = posEnd - posBeg; - mPos += count; - return count; - } + count = posEnd - posBeg; + mPos += count; + return count; + } + catch (std::exception& e) + { + std::stringstream error; + error << "Failed to read '" << mName << "': " << e.what(); + throw std::runtime_error(error.str()); + } + } void skip(long count) { - assert (mPos <= mSize); + assert (mPos <= mSize); if((count >= 0 && (size_t)count <= mSize-mPos) || (count < 0 && (size_t)-count <= mPos)) - mPos += count; + mPos += count; } void seek(size_t pos) { - assert (mPos <= mSize); + assert (mPos <= mSize); if (pos < mSize) - mPos = pos; + mPos = pos; } virtual size_t tell() const { - assert (mPos <= mSize); + assert (mPos <= mSize); - return mPos; - } + return mPos; + } virtual bool eof() const { - assert (mPos <= mSize); + assert (mPos <= mSize); - return mPos == mSize; - } + return mPos == mSize; + } virtual void close() { - mFile.close(); - } + mFile.close(); + } private: - void fill (size_t newOrigin) - { - assert (mFile.tell () == mBufferExtent); + void fill (size_t newOrigin) + { + assert (mFile.tell () == mBufferExtent); - size_t newExtent = newOrigin + sBufferSize; + size_t newExtent = newOrigin + sBufferSize; - if (newExtent > mExtent) - newExtent = mExtent; + if (newExtent > mExtent) + newExtent = mExtent; - size_t oldExtent = mBufferExtent; + size_t oldExtent = mBufferExtent; - if (newOrigin != oldExtent) - mFile.seek (newOrigin); + if (newOrigin != oldExtent) + mFile.seek (newOrigin); - mBufferOrigin = mBufferExtent = newOrigin; + mBufferOrigin = mBufferExtent = newOrigin; - size_t amountRequested = newExtent - newOrigin; + size_t amountRequested = newExtent - newOrigin; - size_t amountRead = mFile.read (mBuffer, amountRequested); + size_t amountRead = mFile.read (mBuffer, amountRequested); - if (amountRead != amountRequested) - throw std::runtime_error ("An unexpected condition occurred while reading from a file."); + if (amountRead != amountRequested) + throw std::runtime_error ("An unexpected condition occurred while reading from a file."); - mBufferExtent = newExtent; - } + mBufferExtent = newExtent; + } - LowLevelFile mFile; + LowLevelFile mFile; - size_t mOrigin; - size_t mExtent; - size_t mPos; + size_t mOrigin; + size_t mExtent; + size_t mPos; - uint8_t mBuffer [sBufferSize]; - size_t mBufferOrigin; - size_t mBufferExtent; + uint8_t mBuffer [sBufferSize]; + size_t mBufferOrigin; + size_t mBufferExtent; }; } // end of unnamed namespace Ogre::DataStreamPtr openConstrainedFileDataStream (char const * filename, size_t offset, size_t length) { - return Ogre::DataStreamPtr(new ConstrainedDataStream(filename, offset, length)); + return Ogre::DataStreamPtr(new ConstrainedDataStream(filename, offset, length)); } diff --git a/components/files/fixedpath.hpp b/components/files/fixedpath.hpp index cfd3458ce..9fb36d984 100644 --- a/components/files/fixedpath.hpp +++ b/components/files/fixedpath.hpp @@ -4,10 +4,14 @@ #include #include -#if defined(__linux__) || defined(__FreeBSD__) +#if defined(__linux__) || defined(__FreeBSD__) || defined(__FreeBSD_kernel__) +#ifndef ANDROID #include namespace Files { typedef LinuxPath TargetPathType; } - +#else + #include + namespace Files { typedef AndroidPath TargetPathType; } +#endif #elif defined(__WIN32) || defined(__WINDOWS__) || defined(_WIN32) #include namespace Files { typedef WindowsPath TargetPathType; } @@ -87,6 +91,7 @@ struct FixedPath return mLocalPath; } + const boost::filesystem::path& getInstallPath() const { return mInstallPath; diff --git a/components/files/linuxpath.cpp b/components/files/linuxpath.cpp index d285f4229..a105bb928 100644 --- a/components/files/linuxpath.cpp +++ b/components/files/linuxpath.cpp @@ -1,6 +1,6 @@ #include "linuxpath.hpp" -#if defined(__linux__) || defined(__FreeBSD__) +#if defined(__linux__) || defined(__FreeBSD__) || defined(__FreeBSD_kernel__) #include #include @@ -69,7 +69,7 @@ boost::filesystem::path LinuxPath::getCachePath() const boost::filesystem::path LinuxPath::getGlobalConfigPath() const { - boost::filesystem::path globalPath("/etc/"); + boost::filesystem::path globalPath(GLOBAL_CONFIG_PATH); return globalPath / mName; } @@ -80,7 +80,7 @@ boost::filesystem::path LinuxPath::getLocalPath() const boost::filesystem::path LinuxPath::getGlobalDataPath() const { - boost::filesystem::path globalDataPath("/usr/share/games/"); + boost::filesystem::path globalDataPath(GLOBAL_DATA_PATH); return globalDataPath / mName; } @@ -157,4 +157,4 @@ boost::filesystem::path LinuxPath::getInstallPath() const } /* namespace Files */ -#endif /* defined(__linux__) || defined(__FreeBSD__) */ +#endif /* defined(__linux__) || defined(__FreeBSD__) || defined(__FreeBSD_kernel__) */ diff --git a/components/files/linuxpath.hpp b/components/files/linuxpath.hpp index b710165b4..ba9756fc0 100644 --- a/components/files/linuxpath.hpp +++ b/components/files/linuxpath.hpp @@ -1,7 +1,7 @@ #ifndef COMPONENTS_FILES_LINUXPATH_H #define COMPONENTS_FILES_LINUXPATH_H -#if defined(__linux__) || defined(__FreeBSD__) +#if defined(__linux__) || defined(__FreeBSD__) || defined(__FreeBSD_kernel__) #include @@ -56,6 +56,6 @@ struct LinuxPath } /* namespace Files */ -#endif /* defined(__linux__) || defined(__FreeBSD__) */ +#endif /* defined(__linux__) || defined(__FreeBSD__) || defined(__FreeBSD_kernel__) */ #endif /* COMPONENTS_FILES_LINUXPATH_H */ diff --git a/components/files/lowlevelfile.cpp b/components/files/lowlevelfile.cpp index 4cdeec043..8456c7a26 100644 --- a/components/files/lowlevelfile.cpp +++ b/components/files/lowlevelfile.cpp @@ -13,196 +13,196 @@ #if FILE_API == FILE_API_STDIO /* * - * Implementation of LowLevelFile methods using c stdio + * Implementation of LowLevelFile methods using c stdio * */ LowLevelFile::LowLevelFile () { - mHandle = NULL; + mHandle = NULL; } LowLevelFile::~LowLevelFile () { - if (mHandle != NULL) - fclose (mHandle); + if (mHandle != NULL) + fclose (mHandle); } void LowLevelFile::open (char const * filename) { - assert (mHandle == NULL); + assert (mHandle == NULL); - mHandle = fopen (filename, "rb"); + mHandle = fopen (filename, "rb"); - if (mHandle == NULL) - { - std::ostringstream os; - os << "Failed to open '" << filename << "' for reading."; - throw std::runtime_error (os.str ()); - } + if (mHandle == NULL) + { + std::ostringstream os; + os << "Failed to open '" << filename << "' for reading."; + throw std::runtime_error (os.str ()); + } } void LowLevelFile::close () { - assert (mHandle != NULL); + assert (mHandle != NULL); - fclose (mHandle); + fclose (mHandle); - mHandle = NULL; + mHandle = NULL; } size_t LowLevelFile::size () { - assert (mHandle != NULL); + assert (mHandle != NULL); - long oldPosition = ftell (mHandle); + long oldPosition = ftell (mHandle); - if (oldPosition == -1) - throw std::runtime_error ("A query operation on a file failed."); + if (oldPosition == -1) + throw std::runtime_error ("A query operation on a file failed."); - if (fseek (mHandle, 0, SEEK_END) != 0) - throw std::runtime_error ("A query operation on a file failed."); + if (fseek (mHandle, 0, SEEK_END) != 0) + throw std::runtime_error ("A query operation on a file failed."); - long Size = ftell (mHandle); + long Size = ftell (mHandle); - if (Size == -1) - throw std::runtime_error ("A query operation on a file failed."); + if (Size == -1) + throw std::runtime_error ("A query operation on a file failed."); - if (fseek (mHandle, oldPosition, SEEK_SET) != 0) - throw std::runtime_error ("A query operation on a file failed."); + if (fseek (mHandle, oldPosition, SEEK_SET) != 0) + throw std::runtime_error ("A query operation on a file failed."); - return size_t (Size); + return size_t (Size); } void LowLevelFile::seek (size_t Position) { - assert (mHandle != NULL); + assert (mHandle != NULL); - if (fseek (mHandle, Position, SEEK_SET) != 0) - throw std::runtime_error ("A seek operation on a file failed."); + if (fseek (mHandle, Position, SEEK_SET) != 0) + throw std::runtime_error ("A seek operation on a file failed."); } size_t LowLevelFile::tell () { - assert (mHandle != NULL); + assert (mHandle != NULL); - long Position = ftell (mHandle); + long Position = ftell (mHandle); - if (Position == -1) - throw std::runtime_error ("A query operation on a file failed."); + if (Position == -1) + throw std::runtime_error ("A query operation on a file failed."); - return size_t (Position); + return size_t (Position); } size_t LowLevelFile::read (void * data, size_t size) { - assert (mHandle != NULL); + assert (mHandle != NULL); - int amount = fread (data, 1, size, mHandle); + int amount = fread (data, 1, size, mHandle); - if (amount == 0 && ferror (mHandle)) - throw std::runtime_error ("A read operation on a file failed."); + if (amount == 0 && ferror (mHandle)) + throw std::runtime_error ("A read operation on a file failed."); - return amount; + return amount; } #elif FILE_API == FILE_API_POSIX /* * - * Implementation of LowLevelFile methods using posix IO calls + * Implementation of LowLevelFile methods using posix IO calls * */ LowLevelFile::LowLevelFile () { - mHandle = -1; + mHandle = -1; } LowLevelFile::~LowLevelFile () { - if (mHandle != -1) - ::close (mHandle); + if (mHandle != -1) + ::close (mHandle); } void LowLevelFile::open (char const * filename) { - assert (mHandle == -1); + assert (mHandle == -1); #ifdef O_BINARY - static const int openFlags = O_RDONLY | O_BINARY; + static const int openFlags = O_RDONLY | O_BINARY; #else - static const int openFlags = O_RDONLY; + static const int openFlags = O_RDONLY; #endif - mHandle = ::open (filename, openFlags, 0); + mHandle = ::open (filename, openFlags, 0); - if (mHandle == -1) - { - std::ostringstream os; - os << "Failed to open '" << filename << "' for reading."; - throw std::runtime_error (os.str ()); - } + if (mHandle == -1) + { + std::ostringstream os; + os << "Failed to open '" << filename << "' for reading."; + throw std::runtime_error (os.str ()); + } } void LowLevelFile::close () { - assert (mHandle != -1); + assert (mHandle != -1); - ::close (mHandle); + ::close (mHandle); - mHandle = -1; + mHandle = -1; } size_t LowLevelFile::size () { - assert (mHandle != -1); + assert (mHandle != -1); - size_t oldPosition = ::lseek (mHandle, 0, SEEK_CUR); + size_t oldPosition = ::lseek (mHandle, 0, SEEK_CUR); - if (oldPosition == size_t (-1)) - throw std::runtime_error ("A query operation on a file failed."); + if (oldPosition == size_t (-1)) + throw std::runtime_error ("A query operation on a file failed."); - size_t Size = ::lseek (mHandle, 0, SEEK_END); + size_t Size = ::lseek (mHandle, 0, SEEK_END); - if (Size == size_t (-1)) - throw std::runtime_error ("A query operation on a file failed."); + if (Size == size_t (-1)) + throw std::runtime_error ("A query operation on a file failed."); - if (lseek (mHandle, oldPosition, SEEK_SET) == -1) - throw std::runtime_error ("A query operation on a file failed."); + if (lseek (mHandle, oldPosition, SEEK_SET) == -1) + throw std::runtime_error ("A query operation on a file failed."); - return Size; + return Size; } void LowLevelFile::seek (size_t Position) { - assert (mHandle != -1); + assert (mHandle != -1); - if (::lseek (mHandle, Position, SEEK_SET) == -1) - throw std::runtime_error ("A seek operation on a file failed."); + if (::lseek (mHandle, Position, SEEK_SET) == -1) + throw std::runtime_error ("A seek operation on a file failed."); } size_t LowLevelFile::tell () { - assert (mHandle != -1); + assert (mHandle != -1); - size_t Position = ::lseek (mHandle, 0, SEEK_CUR); + size_t Position = ::lseek (mHandle, 0, SEEK_CUR); - if (Position == size_t (-1)) - throw std::runtime_error ("A query operation on a file failed."); + if (Position == size_t (-1)) + throw std::runtime_error ("A query operation on a file failed."); - return Position; + return Position; } size_t LowLevelFile::read (void * data, size_t size) { - assert (mHandle != -1); + assert (mHandle != -1); - int amount = ::read (mHandle, data, size); + int amount = ::read (mHandle, data, size); - if (amount == -1) - throw std::runtime_error ("A read operation on a file failed."); + if (amount == -1) + throw std::runtime_error ("A read operation on a file failed."); - return amount; + return amount; } #elif FILE_API == FILE_API_WIN32 @@ -210,93 +210,93 @@ size_t LowLevelFile::read (void * data, size_t size) #include /* * - * Implementation of LowLevelFile methods using Win32 API calls + * Implementation of LowLevelFile methods using Win32 API calls * */ LowLevelFile::LowLevelFile () { - mHandle = INVALID_HANDLE_VALUE; + mHandle = INVALID_HANDLE_VALUE; } LowLevelFile::~LowLevelFile () { - if (mHandle != INVALID_HANDLE_VALUE) - CloseHandle (mHandle); + if (mHandle != INVALID_HANDLE_VALUE) + CloseHandle (mHandle); } void LowLevelFile::open (char const * filename) { - assert (mHandle == INVALID_HANDLE_VALUE); + assert (mHandle == INVALID_HANDLE_VALUE); - std::wstring wname = boost::locale::conv::utf_to_utf(filename); - HANDLE handle = CreateFileW (wname.c_str(), GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, 0, 0); + std::wstring wname = boost::locale::conv::utf_to_utf(filename); + HANDLE handle = CreateFileW (wname.c_str(), GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, 0, 0); - if (handle == INVALID_HANDLE_VALUE) - { - std::ostringstream os; - os << "Failed to open '" << filename << "' for reading."; - throw std::runtime_error (os.str ()); - } + if (handle == INVALID_HANDLE_VALUE) + { + std::ostringstream os; + os << "Failed to open '" << filename << "' for reading."; + throw std::runtime_error (os.str ()); + } - mHandle = handle; + mHandle = handle; } void LowLevelFile::close () { - assert (mHandle != INVALID_HANDLE_VALUE); + assert (mHandle != INVALID_HANDLE_VALUE); - CloseHandle (mHandle); + CloseHandle (mHandle); - mHandle = INVALID_HANDLE_VALUE; + mHandle = INVALID_HANDLE_VALUE; } size_t LowLevelFile::size () { - assert (mHandle != INVALID_HANDLE_VALUE); + assert (mHandle != INVALID_HANDLE_VALUE); - BY_HANDLE_FILE_INFORMATION info; + BY_HANDLE_FILE_INFORMATION info; - if (!GetFileInformationByHandle (mHandle, &info)) - throw std::runtime_error ("A query operation on a file failed."); + if (!GetFileInformationByHandle (mHandle, &info)) + throw std::runtime_error ("A query operation on a file failed."); - if (info.nFileSizeHigh != 0) - throw std::runtime_error ("Files greater that 4GB are not supported."); + if (info.nFileSizeHigh != 0) + throw std::runtime_error ("Files greater that 4GB are not supported."); - return info.nFileSizeLow; + return info.nFileSizeLow; } void LowLevelFile::seek (size_t Position) { - assert (mHandle != INVALID_HANDLE_VALUE); + assert (mHandle != INVALID_HANDLE_VALUE); - if (SetFilePointer (mHandle, Position, NULL, SEEK_SET) == INVALID_SET_FILE_POINTER) - if (GetLastError () != NO_ERROR) - throw std::runtime_error ("A seek operation on a file failed."); + if (SetFilePointer (mHandle, Position, NULL, SEEK_SET) == INVALID_SET_FILE_POINTER) + if (GetLastError () != NO_ERROR) + throw std::runtime_error ("A seek operation on a file failed."); } size_t LowLevelFile::tell () { - assert (mHandle != INVALID_HANDLE_VALUE); + assert (mHandle != INVALID_HANDLE_VALUE); - DWORD value = SetFilePointer (mHandle, 0, NULL, SEEK_CUR); + DWORD value = SetFilePointer (mHandle, 0, NULL, SEEK_CUR); - if (value == INVALID_SET_FILE_POINTER && GetLastError () != NO_ERROR) - throw std::runtime_error ("A query operation on a file failed."); + if (value == INVALID_SET_FILE_POINTER && GetLastError () != NO_ERROR) + throw std::runtime_error ("A query operation on a file failed."); - return value; + return value; } size_t LowLevelFile::read (void * data, size_t size) { - assert (mHandle != INVALID_HANDLE_VALUE); + assert (mHandle != INVALID_HANDLE_VALUE); - DWORD read; + DWORD read; - if (!ReadFile (mHandle, data, size, &read, NULL)) - throw std::runtime_error ("A read operation on a file failed."); + if (!ReadFile (mHandle, data, size, &read, NULL)) + throw std::runtime_error ("A read operation on a file failed."); - return read; + return read; } #endif diff --git a/components/files/lowlevelfile.hpp b/components/files/lowlevelfile.hpp index f49c466a5..d94238ad6 100644 --- a/components/files/lowlevelfile.hpp +++ b/components/files/lowlevelfile.hpp @@ -5,16 +5,16 @@ #include -#define FILE_API_STDIO 0 -#define FILE_API_POSIX 1 -#define FILE_API_WIN32 2 +#define FILE_API_STDIO 0 +#define FILE_API_POSIX 1 +#define FILE_API_WIN32 2 #if OGRE_PLATFORM == OGRE_PLATFORM_LINUX -#define FILE_API FILE_API_POSIX +#define FILE_API FILE_API_POSIX #elif OGRE_PLATFORM == OGRE_PLATFORM_WIN32 -#define FILE_API FILE_API_WIN32 +#define FILE_API FILE_API_WIN32 #else -#define FILE_API FILE_API_STDIO +#define FILE_API FILE_API_STDIO #endif #if FILE_API == FILE_API_STDIO @@ -30,26 +30,26 @@ class LowLevelFile { public: - LowLevelFile (); - ~LowLevelFile (); + LowLevelFile (); + ~LowLevelFile (); - void open (char const * filename); - void close (); + void open (char const * filename); + void close (); - size_t size (); + size_t size (); - void seek (size_t Position); - size_t tell (); + void seek (size_t Position); + size_t tell (); - size_t read (void * data, size_t size); + size_t read (void * data, size_t size); private: #if FILE_API == FILE_API_STDIO - FILE* mHandle; + FILE* mHandle; #elif FILE_API == FILE_API_POSIX - int mHandle; + int mHandle; #elif FILE_API == FILE_API_WIN32 - HANDLE mHandle; + HANDLE mHandle; #endif }; diff --git a/apps/openmw/mwgui/fontloader.cpp b/components/fontloader/fontloader.cpp similarity index 74% rename from apps/openmw/mwgui/fontloader.cpp rename to components/fontloader/fontloader.cpp index 92d9a25b6..17c630fd9 100644 --- a/apps/openmw/mwgui/fontloader.cpp +++ b/components/fontloader/fontloader.cpp @@ -1,5 +1,7 @@ #include "fontloader.hpp" +#include + #include #include @@ -121,9 +123,18 @@ namespace return encoder.getUtf8(std::string(1, c)); } + void fail (Ogre::DataStreamPtr file, const std::string& fileName, const std::string& message) + { + std::stringstream error; + error << "Font loading error: " << message; + error << "\n File: " << fileName; + error << "\n Offset: 0x" << std::hex << file->tell(); + throw std::runtime_error(error.str()); + } + } -namespace MWGui +namespace Gui { FontLoader::FontLoader(ToUTF8::FromType encoding) @@ -134,7 +145,7 @@ namespace MWGui mEncoding = encoding; } - void FontLoader::loadAllFonts() + void FontLoader::loadAllFonts(bool exportToFile) { Ogre::StringVector groups = Ogre::ResourceGroupManager::getSingleton().getResourceGroups (); for (Ogre::StringVector::iterator it = groups.begin(); it != groups.end(); ++it) @@ -142,7 +153,7 @@ namespace MWGui Ogre::StringVectorPtr resourcesInThisGroup = Ogre::ResourceGroupManager::getSingleton ().findResourceNames (*it, "*.fnt"); for (Ogre::StringVector::iterator resource = resourcesInThisGroup->begin(); resource != resourcesInThisGroup->end(); ++resource) { - loadFont(*resource); + loadFont(*resource, exportToFile); } } } @@ -168,25 +179,35 @@ namespace MWGui float ascent; } GlyphInfo; - void FontLoader::loadFont(const std::string &fileName) + void FontLoader::loadFont(const std::string &fileName, bool exportToFile) { Ogre::DataStreamPtr file = Ogre::ResourceGroupManager::getSingleton().openResource(fileName); float fontSize; + if (file->read(&fontSize, sizeof(fontSize)) < sizeof(fontSize)) + fail(file, fileName, "File too small to be a valid font"); + int one; - file->read(&fontSize, sizeof(fontSize)); + if (file->read(&one, sizeof(int)) < sizeof(int)) + fail(file, fileName, "File too small to be a valid font"); + + if (one != 1) + fail(file, fileName, "Unexpected value"); + + if (file->read(&one, sizeof(int)) < sizeof(int)) + fail(file, fileName, "File too small to be a valid font"); - file->read(&one, sizeof(int)); - assert(one == 1); - file->read(&one, sizeof(int)); - assert(one == 1); + if (one != 1) + fail(file, fileName, "Unexpected value"); char name_[284]; - file->read(name_, sizeof(name_)); + if (file->read(name_, sizeof(name_)) < sizeof(name_)) + fail(file, fileName, "File too small to be a valid font"); std::string name(name_); GlyphInfo data[256]; - file->read(data, sizeof(data)); + if (file->read(data, sizeof(data)) < sizeof(data)) + fail(file, fileName, "File too small to be a valid font"); file->close(); // Create the font texture @@ -194,12 +215,19 @@ namespace MWGui Ogre::DataStreamPtr bitmapFile = Ogre::ResourceGroupManager::getSingleton().openResource(bitmapFilename); int width, height; - bitmapFile->read(&width, sizeof(int)); - bitmapFile->read(&height, sizeof(int)); + if (bitmapFile->read(&width, sizeof(int)) < sizeof(int)) + fail(bitmapFile, bitmapFilename, "File too small to be a valid bitmap"); + + if (bitmapFile->read(&height, sizeof(int)) < sizeof(int)) + fail(bitmapFile, bitmapFilename, "File too small to be a valid bitmap"); + + if (width <= 0 || height <= 0) + fail(bitmapFile, bitmapFilename, "Width and height must be positive"); std::vector textureData; textureData.resize(width*height*4); - bitmapFile->read(&textureData[0], width*height*4); + if (bitmapFile->read(&textureData[0], width*height*4) < (size_t)(width*height*4)) + fail(bitmapFile, bitmapFilename, "Bitmap does not contain the specified number of pixels"); bitmapFile->close(); std::string resourceName; @@ -221,6 +249,9 @@ namespace MWGui width, height, 0, Ogre::PF_BYTE_RGBA); texture->loadImage(image); + if (exportToFile) + image.save(resourceName + ".png"); + // Register the font with MyGUI MyGUI::ResourceManualFont* font = static_cast( MyGUI::FactoryManager::getInstance().createObject("Resource", "ResourceManualFont")); @@ -240,10 +271,10 @@ namespace MWGui for(int i = 0; i < 256; i++) { - int x1 = data[i].top_left.x*width; - int y1 = data[i].top_left.y*height; - int w = data[i].top_right.x*width - x1; - int h = data[i].bottom_left.y*height - y1; + float x1 = data[i].top_left.x*width; + float y1 = data[i].top_left.y*height; + float w = data[i].top_right.x*width - x1; + float h = data[i].bottom_left.y*height - y1; ToUTF8::Utf8Encoder encoder(mEncoding); unsigned long unicodeVal = utf8ToUnicode(getUtf8(i, encoder, mEncoding)); @@ -257,16 +288,38 @@ namespace MWGui code->addAttribute("advance", data[i].width); code->addAttribute("bearing", MyGUI::utility::toString(data[i].kerning) + " " + MyGUI::utility::toString((fontSize-data[i].ascent))); + code->addAttribute("size", MyGUI::IntSize(data[i].width, data[i].height)); // More hacks! The french game uses several win1252 characters that are not included // in the cp437 encoding of the font. Fall back to similar available characters. if (mEncoding == ToUTF8::CP437) { - std::multimap additional; + std::multimap additional; // additional.insert(std::make_pair(39, 0x2019)); // apostrophe additional.insert(std::make_pair(45, 0x2013)); // dash + additional.insert(std::make_pair(45, 0x2014)); // dash additional.insert(std::make_pair(34, 0x201D)); // right double quotation mark additional.insert(std::make_pair(34, 0x201C)); // left double quotation mark + additional.insert(std::make_pair(44, 0x201A)); + additional.insert(std::make_pair(44, 0x201E)); + additional.insert(std::make_pair(43, 0x2020)); + additional.insert(std::make_pair(94, 0x02C6)); + additional.insert(std::make_pair(37, 0x2030)); + additional.insert(std::make_pair(83, 0x0160)); + additional.insert(std::make_pair(60, 0x2039)); + additional.insert(std::make_pair(79, 0x0152)); + additional.insert(std::make_pair(90, 0x017D)); + additional.insert(std::make_pair(39, 0x2019)); + additional.insert(std::make_pair(126, 0x02DC)); + additional.insert(std::make_pair(84, 0x2122)); + additional.insert(std::make_pair(83, 0x0161)); + additional.insert(std::make_pair(62, 0x203A)); + additional.insert(std::make_pair(111, 0x0153)); + additional.insert(std::make_pair(122, 0x017E)); + additional.insert(std::make_pair(89, 0x0178)); + additional.insert(std::make_pair(156, 0x00A2)); + additional.insert(std::make_pair(46, 0x2026)); + for (std::multimap::iterator it = additional.begin(); it != additional.end(); ++it) { if (it->first != i) @@ -281,6 +334,7 @@ namespace MWGui code->addAttribute("advance", data[i].width); code->addAttribute("bearing", MyGUI::utility::toString(data[i].kerning) + " " + MyGUI::utility::toString((fontSize-data[i].ascent))); + code->addAttribute("size", MyGUI::IntSize(data[i].width, data[i].height)); } } @@ -296,6 +350,7 @@ namespace MWGui cursorCode->addAttribute("advance", data[i].width); cursorCode->addAttribute("bearing", MyGUI::utility::toString(data[i].kerning) + " " + MyGUI::utility::toString((fontSize-data[i].ascent))); + cursorCode->addAttribute("size", MyGUI::IntSize(data[i].width, data[i].height)); } // Question mark, use for NotDefined marker (used for glyphs not existing in the font) @@ -310,6 +365,7 @@ namespace MWGui cursorCode->addAttribute("advance", data[i].width); cursorCode->addAttribute("bearing", MyGUI::utility::toString(data[i].kerning) + " " + MyGUI::utility::toString((fontSize-data[i].ascent))); + cursorCode->addAttribute("size", MyGUI::IntSize(data[i].width, data[i].height)); } } @@ -327,7 +383,13 @@ namespace MWGui cursorCode->addAttribute("coord", "0 0 0 0"); cursorCode->addAttribute("advance", "0"); cursorCode->addAttribute("bearing", "0 0"); + cursorCode->addAttribute("size", "0 0"); + } + if (exportToFile) + { + xmlDocument.createDeclaration(); + xmlDocument.save(resourceName + ".xml"); } font->deserialization(root, MyGUI::Version(3,2,0)); diff --git a/components/fontloader/fontloader.hpp b/components/fontloader/fontloader.hpp new file mode 100644 index 000000000..a41506dbb --- /dev/null +++ b/components/fontloader/fontloader.hpp @@ -0,0 +1,28 @@ +#ifndef MWGUI_FONTLOADER_H +#define MWGUI_FONTLOADER_H + +#include + +namespace Gui +{ + + + /// @brief loads Morrowind's .fnt/.tex fonts for use with MyGUI and Ogre + class FontLoader + { + public: + FontLoader (ToUTF8::FromType encoding); + + /// @param exportToFile export the converted fonts (Images and XML with glyph metrics) to files? + void loadAllFonts (bool exportToFile); + + private: + ToUTF8::FromType mEncoding; + + /// @param exportToFile export the converted font (Image and XML with glyph metrics) to files? + void loadFont (const std::string& fileName, bool exportToFile); + }; + +} + +#endif diff --git a/components/interpreter/context.hpp b/components/interpreter/context.hpp index 97e4fad4f..881687366 100644 --- a/components/interpreter/context.hpp +++ b/components/interpreter/context.hpp @@ -81,7 +81,7 @@ namespace Interpreter virtual bool isScriptRunning (const std::string& name) const = 0; - virtual void startScript (const std::string& name) = 0; + virtual void startScript (const std::string& name, const std::string& targetId = "") = 0; virtual void stopScript (const std::string& name) = 0; @@ -108,6 +108,8 @@ namespace Interpreter virtual void setMemberFloat (const std::string& id, const std::string& name, float value, bool global) = 0; + + virtual std::string getTargetId() const = 0; }; } diff --git a/components/interpreter/defines.cpp b/components/interpreter/defines.cpp index 6f3b5bb5a..4881274f0 100644 --- a/components/interpreter/defines.cpp +++ b/components/interpreter/defines.cpp @@ -4,10 +4,12 @@ #include #include #include +#include namespace Interpreter{ - bool Check(const std::string& str, const std::string& escword, unsigned int* i, unsigned int* start){ + bool check(const std::string& str, const std::string& escword, unsigned int* i, unsigned int* start) + { bool retval = str.find(escword) == 0; if(retval){ (*i) += escword.length(); @@ -18,170 +20,181 @@ namespace Interpreter{ std::vector globals; - bool longerStr(const std::string& a, const std::string& b){ + bool longerStr(const std::string& a, const std::string& b) + { return a.length() > b.length(); } - std::string fixDefinesReal(std::string text, char eschar, bool isBook, Context& context){ + std::string fixDefinesReal(std::string text, char eschar, bool isBook, Context& context) + { unsigned int start = 0; std::ostringstream retval; - for(unsigned int i = 0; i < text.length(); i++){ - if(text[i] == eschar){ + for(unsigned int i = 0; i < text.length(); i++) + { + if(text[i] == eschar) + { retval << text.substr(start, i - start); std::string temp = text.substr(i+1, 100); transform(temp.begin(), temp.end(), temp.begin(), ::tolower); - bool found; - - if( (found = Check(temp, "actionslideright", &i, &start))){ - retval << context.getActionBinding("#{sRight}"); - } - else if((found = Check(temp, "actionreadymagic", &i, &start))){ - retval << context.getActionBinding("#{sReady_Magic}"); - } - else if((found = Check(temp, "actionprevweapon", &i, &start))){ - retval << "PLACEHOLDER_ACTION_PREV_WEAPON"; - } - else if((found = Check(temp, "actionnextweapon", &i, &start))){ - retval << "PLACEHOLDER_ACTION_PREV_WEAPON"; - } - else if((found = Check(temp, "actiontogglerun", &i, &start))){ - retval << context.getActionBinding("#{sAuto_Run}"); - } - else if((found = Check(temp, "actionslideleft", &i, &start))){ - retval << context.getActionBinding("#{sLeft}"); - } - else if((found = Check(temp, "actionreadyitem", &i, &start))){ - retval << context.getActionBinding("#{sReady_Weapon}"); - } - else if((found = Check(temp, "actionprevspell", &i, &start))){ - retval << "PLACEHOLDER_ACTION_PREV_SPELL"; - } - else if((found = Check(temp, "actionnextspell", &i, &start))){ - retval << "PLACEHOLDER_ACTION_NEXT_SPELL"; - } - else if((found = Check(temp, "actionrestmenu", &i, &start))){ - retval << context.getActionBinding("#{sRestKey}"); - } - else if((found = Check(temp, "actionmenumode", &i, &start))){ - retval << context.getActionBinding("#{sInventory}"); - } - else if((found = Check(temp, "actionactivate", &i, &start))){ - retval << context.getActionBinding("#{sActivate}"); - } - else if((found = Check(temp, "actionjournal", &i, &start))){ - retval << context.getActionBinding("#{sJournal}"); - } - else if((found = Check(temp, "actionforward", &i, &start))){ - retval << context.getActionBinding("#{sForward}"); - } - else if((found = Check(temp, "pccrimelevel", &i, &start))){ - retval << context.getPCBounty(); - } - else if((found = Check(temp, "actioncrouch", &i, &start))){ - retval << context.getActionBinding("#{sCrouch_Sneak}"); - } - else if((found = Check(temp, "actionjump", &i, &start))){ - retval << context.getActionBinding("#{sJump}"); - } - else if((found = Check(temp, "actionback", &i, &start))){ - retval << context.getActionBinding("#{sBack}"); - } - else if((found = Check(temp, "actionuse", &i, &start))){ - retval << context.getActionBinding("#{sUse}"); - } - else if((found = Check(temp, "actionrun", &i, &start))){ - retval << context.getActionBinding("#{sRun}"); - } - else if((found = Check(temp, "pcclass", &i, &start))){ - retval << context.getPCClass(); - } - else if((found = Check(temp, "pcrace", &i, &start))){ - retval << context.getPCRace(); - } - else if((found = Check(temp, "pcname", &i, &start))){ - retval << context.getPCName(); - } - else if((found = Check(temp, "cell", &i, &start))){ - retval << context.getCurrentCellName(); - } - - else if(eschar == '%' && !isBook) { // In Dialogue, not messagebox - if( (found = Check(temp, "faction", &i, &start))){ - retval << context.getNPCFaction(); + bool found = false; + try + { + if( (found = check(temp, "actionslideright", &i, &start))){ + retval << context.getActionBinding("#{sRight}"); } - else if((found = Check(temp, "nextpcrank", &i, &start))){ - retval << context.getPCNextRank(); + else if((found = check(temp, "actionreadymagic", &i, &start))){ + retval << context.getActionBinding("#{sReady_Magic}"); } - else if((found = Check(temp, "pcnextrank", &i, &start))){ - retval << context.getPCNextRank(); + else if((found = check(temp, "actionprevweapon", &i, &start))){ + retval << context.getActionBinding("#{sPrevWeapon}"); } - else if((found = Check(temp, "pcrank", &i, &start))){ - retval << context.getPCRank(); + else if((found = check(temp, "actionnextweapon", &i, &start))){ + retval << context.getActionBinding("#{sNextWeapon}"); } - else if((found = Check(temp, "rank", &i, &start))){ - retval << context.getNPCRank(); + else if((found = check(temp, "actiontogglerun", &i, &start))){ + retval << context.getActionBinding("#{sAuto_Run}"); } - - else if((found = Check(temp, "class", &i, &start))){ - retval << context.getNPCClass(); + else if((found = check(temp, "actionslideleft", &i, &start))){ + retval << context.getActionBinding("#{sLeft}"); } - else if((found = Check(temp, "race", &i, &start))){ - retval << context.getNPCRace(); + else if((found = check(temp, "actionreadyitem", &i, &start))){ + retval << context.getActionBinding("#{sReady_Weapon}"); } - else if((found = Check(temp, "name", &i, &start))){ - retval << context.getNPCName(); + else if((found = check(temp, "actionprevspell", &i, &start))){ + retval << context.getActionBinding("#{sPrevSpell}"); } - } - else { // In messagebox or book, not dialogue - - /* empty outside dialogue */ - if( (found = Check(temp, "faction", &i, &start))); - else if((found = Check(temp, "nextpcrank", &i, &start))); - else if((found = Check(temp, "pcnextrank", &i, &start))); - else if((found = Check(temp, "pcrank", &i, &start))); - else if((found = Check(temp, "rank", &i, &start))); - - /* uses pc in messageboxes */ - else if((found = Check(temp, "class", &i, &start))){ + else if((found = check(temp, "actionnextspell", &i, &start))){ + retval << context.getActionBinding("#{sNextSpell}"); + } + else if((found = check(temp, "actionrestmenu", &i, &start))){ + retval << context.getActionBinding("#{sRestKey}"); + } + else if((found = check(temp, "actionmenumode", &i, &start))){ + retval << context.getActionBinding("#{sInventory}"); + } + else if((found = check(temp, "actionactivate", &i, &start))){ + retval << context.getActionBinding("#{sActivate}"); + } + else if((found = check(temp, "actionjournal", &i, &start))){ + retval << context.getActionBinding("#{sJournal}"); + } + else if((found = check(temp, "actionforward", &i, &start))){ + retval << context.getActionBinding("#{sForward}"); + } + else if((found = check(temp, "pccrimelevel", &i, &start))){ + retval << context.getPCBounty(); + } + else if((found = check(temp, "actioncrouch", &i, &start))){ + retval << context.getActionBinding("#{sCrouch_Sneak}"); + } + else if((found = check(temp, "actionjump", &i, &start))){ + retval << context.getActionBinding("#{sJump}"); + } + else if((found = check(temp, "actionback", &i, &start))){ + retval << context.getActionBinding("#{sBack}"); + } + else if((found = check(temp, "actionuse", &i, &start))){ + retval << context.getActionBinding("#{sUse}"); + } + else if((found = check(temp, "actionrun", &i, &start))){ + retval << context.getActionBinding("#{sRun}"); + } + else if((found = check(temp, "pcclass", &i, &start))){ retval << context.getPCClass(); } - else if((found = Check(temp, "race", &i, &start))){ + else if((found = check(temp, "pcrace", &i, &start))){ retval << context.getPCRace(); } - else if((found = Check(temp, "name", &i, &start))){ + else if((found = check(temp, "pcname", &i, &start))){ retval << context.getPCName(); } - } - - /* Not a builtin, try global variables */ - if(!found){ - /* if list of globals is empty, grab it and sort it by descending string length */ - if(globals.empty()){ - globals = context.getGlobals(); - sort(globals.begin(), globals.end(), longerStr); + else if((found = check(temp, "cell", &i, &start))){ + retval << context.getCurrentCellName(); } - for(unsigned int j = 0; j < globals.size(); j++){ - if(globals[j].length() > temp.length()){ // Just in case there's a global with a huuuge name - std::string temp = text.substr(i+1, globals[j].length()); - transform(temp.begin(), temp.end(), temp.begin(), ::tolower); + else if(eschar == '%' && !isBook) { // In Dialogue, not messagebox + if( (found = check(temp, "faction", &i, &start))){ + retval << context.getNPCFaction(); + } + else if((found = check(temp, "nextpcrank", &i, &start))){ + retval << context.getPCNextRank(); + } + else if((found = check(temp, "pcnextrank", &i, &start))){ + retval << context.getPCNextRank(); + } + else if((found = check(temp, "pcrank", &i, &start))){ + retval << context.getPCRank(); + } + else if((found = check(temp, "rank", &i, &start))){ + retval << context.getNPCRank(); } - if((found = Check(temp, globals[j], &i, &start))){ - char type = context.getGlobalType(globals[j]); + else if((found = check(temp, "class", &i, &start))){ + retval << context.getNPCClass(); + } + else if((found = check(temp, "race", &i, &start))){ + retval << context.getNPCRace(); + } + else if((found = check(temp, "name", &i, &start))){ + retval << context.getNPCName(); + } + } + else { // In messagebox or book, not dialogue + + /* empty outside dialogue */ + if( (found = check(temp, "faction", &i, &start))); + else if((found = check(temp, "nextpcrank", &i, &start))); + else if((found = check(temp, "pcnextrank", &i, &start))); + else if((found = check(temp, "pcrank", &i, &start))); + else if((found = check(temp, "rank", &i, &start))); + + /* uses pc in messageboxes */ + else if((found = check(temp, "class", &i, &start))){ + retval << context.getPCClass(); + } + else if((found = check(temp, "race", &i, &start))){ + retval << context.getPCRace(); + } + else if((found = check(temp, "name", &i, &start))){ + retval << context.getPCName(); + } + } - switch(type){ - case 's': retval << context.getGlobalShort(globals[j]); break; - case 'l': retval << context.getGlobalLong(globals[j]); break; - case 'f': retval << context.getGlobalFloat(globals[j]); break; + /* Not a builtin, try global variables */ + if(!found){ + /* if list of globals is empty, grab it and sort it by descending string length */ + if(globals.empty()){ + globals = context.getGlobals(); + sort(globals.begin(), globals.end(), longerStr); + } + + for(unsigned int j = 0; j < globals.size(); j++){ + if(globals[j].length() > temp.length()){ // Just in case there's a global with a huuuge name + std::string temp = text.substr(i+1, globals[j].length()); + transform(temp.begin(), temp.end(), temp.begin(), ::tolower); + } + + if((found = check(temp, globals[j], &i, &start))){ + char type = context.getGlobalType(globals[j]); + + switch(type){ + case 's': retval << context.getGlobalShort(globals[j]); break; + case 'l': retval << context.getGlobalLong(globals[j]); break; + case 'f': retval << context.getGlobalFloat(globals[j]); break; + } + break; } - break; } } } - - /* Not found */ + catch (std::exception& e) + { + std::cerr << "Failed to replace escape character, with the following error: " << e.what() << std::endl; + std::cerr << "Full text below: " << std::endl << text << std::endl; + } + + // Not found, or error if(!found){ /* leave unmodified */ i += 1; diff --git a/components/interpreter/docs/vmformat.txt b/components/interpreter/docs/vmformat.txt index 990762268..5d1eba088 100644 --- a/components/interpreter/docs/vmformat.txt +++ b/components/interpreter/docs/vmformat.txt @@ -133,5 +133,6 @@ op 67: store stack[0] in member float stack[2] of global script with ID stack[1] op 68: replace stack[0] with member short stack[1] of global script with ID stack[0] op 69: replace stack[0] with member short stack[1] of global script with ID stack[0] op 70: replace stack[0] with member short stack[1] of global script with ID stack[0] -opcodes 71-33554431 unused +op 71: explicit reference (target) = stack[0]; pop; start script stack[0] and pop +opcodes 72-33554431 unused opcodes 33554432-67108863 reserved for extensions diff --git a/components/interpreter/installopcodes.cpp b/components/interpreter/installopcodes.cpp index 721cde3d8..d705a109c 100644 --- a/components/interpreter/installopcodes.cpp +++ b/components/interpreter/installopcodes.cpp @@ -113,6 +113,7 @@ namespace Interpreter interpreter.installSegment5 (46, new OpScriptRunning); interpreter.installSegment5 (47, new OpStartScript); interpreter.installSegment5 (48, new OpStopScript); + interpreter.installSegment5 (71, new OpStartScriptExplicit); // spacial interpreter.installSegment5 (49, new OpGetDistance); diff --git a/components/interpreter/scriptopcodes.hpp b/components/interpreter/scriptopcodes.hpp index 56502d510..976390eb5 100644 --- a/components/interpreter/scriptopcodes.hpp +++ b/components/interpreter/scriptopcodes.hpp @@ -10,36 +10,52 @@ namespace Interpreter class OpScriptRunning : public Opcode0 { public: - + virtual void execute (Runtime& runtime) { std::string name = runtime.getStringLiteral (runtime[0].mInteger); runtime[0].mInteger = runtime.getContext().isScriptRunning (name); - } + } }; class OpStartScript : public Opcode0 { public: - + virtual void execute (Runtime& runtime) { std::string name = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); - runtime.getContext().startScript (name); - } + runtime.getContext().startScript (name, runtime.getContext().getTargetId()); + } + }; + + class OpStartScriptExplicit : public Opcode0 + { + public: + + virtual void execute (Runtime& runtime) + { + std::string targetId = runtime.getStringLiteral (runtime[0].mInteger); + runtime.pop(); + + std::string name = runtime.getStringLiteral (runtime[0].mInteger); + runtime.pop(); + + runtime.getContext().startScript (name, targetId); + } }; class OpStopScript : public Opcode0 { public: - + virtual void execute (Runtime& runtime) { std::string name = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); runtime.getContext().stopScript (name); - } + } }; } diff --git a/components/misc/resourcehelpers.cpp b/components/misc/resourcehelpers.cpp new file mode 100644 index 000000000..dc08b352a --- /dev/null +++ b/components/misc/resourcehelpers.cpp @@ -0,0 +1,140 @@ +#include "resourcehelpers.hpp" + +#include + +#include + +namespace +{ + + + struct MatchPathSeparator + { + bool operator()( char ch ) const + { + return ch == '\\' || ch == '/'; + } + }; + + std::string + getBasename( std::string const& pathname ) + { + return std::string( + std::find_if( pathname.rbegin(), pathname.rend(), + MatchPathSeparator() ).base(), + pathname.end() ); + } + +} + +bool Misc::ResourceHelpers::changeExtensionToDds(std::string &path) +{ + Ogre::String::size_type pos = path.rfind('.'); + if(pos != Ogre::String::npos && path.compare(pos, path.length() - pos, ".dds") != 0) + { + path.replace(pos, path.length(), ".dds"); + return true; + } + return false; +} + +std::string Misc::ResourceHelpers::correctResourcePath(const std::string &topLevelDirectory, const std::string &resPath) +{ + /* Bethesda at some point converted all their BSA + * textures from tga to dds for increased load speed, but all + * texture file name references were kept as .tga. + */ + + std::string prefix1 = topLevelDirectory + '\\'; + std::string prefix2 = topLevelDirectory + '/'; + + std::string correctedPath = resPath; + Misc::StringUtils::toLower(correctedPath); + + // Apparently, leading separators are allowed + while (correctedPath.size() && (correctedPath[0] == '/' || correctedPath[0] == '\\')) + correctedPath.erase(0, 1); + + if(correctedPath.compare(0, prefix1.size(), prefix1.data()) != 0 && + correctedPath.compare(0, prefix2.size(), prefix2.data()) != 0) + correctedPath = prefix1 + correctedPath; + + std::string origExt = correctedPath; + + // since we know all (GOTY edition or less) textures end + // in .dds, we change the extension + bool changedToDds = changeExtensionToDds(correctedPath); + if (Ogre::ResourceGroupManager::getSingleton().resourceExistsInAnyGroup(correctedPath)) + return correctedPath; + // if it turns out that the above wasn't true in all cases (not for vanilla, but maybe mods) + // verify, and revert if false (this call succeeds quickly, but fails slowly) + if (changedToDds && Ogre::ResourceGroupManager::getSingleton().resourceExistsInAnyGroup(origExt)) + return origExt; + + // fall back to a resource in the top level directory if it exists + std::string fallback = topLevelDirectory + "\\" + getBasename(correctedPath); + if (Ogre::ResourceGroupManager::getSingleton().resourceExistsInAnyGroup(fallback)) + return fallback; + + if (changedToDds) + { + fallback = topLevelDirectory + "\\" + getBasename(origExt); + if (Ogre::ResourceGroupManager::getSingleton().resourceExistsInAnyGroup(fallback)) + return fallback; + } + + return correctedPath; +} + +std::string Misc::ResourceHelpers::correctTexturePath(const std::string &resPath) +{ + static const std::string dir = "textures"; + return correctResourcePath(dir, resPath); +} + +std::string Misc::ResourceHelpers::correctIconPath(const std::string &resPath) +{ + static const std::string dir = "icons"; + return correctResourcePath(dir, resPath); +} + +std::string Misc::ResourceHelpers::correctBookartPath(const std::string &resPath) +{ + static const std::string dir = "bookart"; + std::string image = correctResourcePath(dir, resPath); + + return image; +} + +std::string Misc::ResourceHelpers::correctBookartPath(const std::string &resPath, int width, int height) +{ + std::string image = correctBookartPath(resPath); + + // Apparently a bug with some morrowind versions, they reference the image without the size suffix. + // So if the image isn't found, try appending the size. + if (!Ogre::ResourceGroupManager::getSingleton().resourceExistsInAnyGroup(image)) + { + std::stringstream str; + str << image.substr(0, image.rfind('.')) << "_" << width << "_" << height << image.substr(image.rfind('.')); + image = Misc::ResourceHelpers::correctBookartPath(str.str()); + } + + return image; +} + +std::string Misc::ResourceHelpers::correctActorModelPath(const std::string &resPath) +{ + std::string mdlname = resPath; + std::string::size_type p = mdlname.rfind('\\'); + if(p == std::string::npos) + p = mdlname.rfind('/'); + if(p != std::string::npos) + mdlname.insert(mdlname.begin()+p+1, 'x'); + else + mdlname.insert(mdlname.begin(), 'x'); + if(!Ogre::ResourceGroupManager::getSingleton().resourceExistsInAnyGroup(mdlname)) + { + return resPath; + } + return mdlname; +} diff --git a/components/misc/resourcehelpers.hpp b/components/misc/resourcehelpers.hpp new file mode 100644 index 000000000..2ce3dce1e --- /dev/null +++ b/components/misc/resourcehelpers.hpp @@ -0,0 +1,21 @@ +#ifndef MISC_RESOURCEHELPERS_H +#define MISC_RESOURCEHELPERS_H + +#include + +namespace Misc +{ + namespace ResourceHelpers + { + bool changeExtensionToDds(std::string &path); + std::string correctResourcePath(const std::string &topLevelDirectory, const std::string &resPath); + std::string correctTexturePath(const std::string &resPath); + std::string correctIconPath(const std::string &resPath); + std::string correctBookartPath(const std::string &resPath); + std::string correctBookartPath(const std::string &resPath, int width, int height); + /// Uses "xfoo.nif" instead of "foo.nif" if available + std::string correctActorModelPath(const std::string &resPath); + } +} + +#endif diff --git a/components/misc/stringops.cpp b/components/misc/stringops.cpp index 0f801e554..723c1c1e0 100644 --- a/components/misc/stringops.cpp +++ b/components/misc/stringops.cpp @@ -1,14 +1,5 @@ #include "stringops.hpp" -#include -#include -#include - -#include -#include - - - namespace Misc { diff --git a/components/misc/utf8stream.hpp b/components/misc/utf8stream.hpp index 5e9dde251..4856d6503 100644 --- a/components/misc/utf8stream.hpp +++ b/components/misc/utf8stream.hpp @@ -73,7 +73,7 @@ public: while (cur != eoc) { if ((*cur & 0xC0) != 0x80) // check continuation mark - return std::make_pair (sBadChar(), cur);; + return std::make_pair (sBadChar(), cur); chr = (chr << 6) | UnicodeChar ((*cur++) & 0x3F); } diff --git a/components/nif/.gitignore b/components/nif/.gitignore deleted file mode 100644 index 731498d9a..000000000 --- a/components/nif/.gitignore +++ /dev/null @@ -1 +0,0 @@ -old_d diff --git a/components/nif/base.hpp b/components/nif/base.hpp new file mode 100644 index 000000000..30c652b64 --- /dev/null +++ b/components/nif/base.hpp @@ -0,0 +1,91 @@ +///This file holds the main classes of NIF Records used by everything else. +#ifndef OPENMW_COMPONENTS_NIF_BASE_HPP +#define OPENMW_COMPONENTS_NIF_BASE_HPP + +#include "record.hpp" +#include "niffile.hpp" +#include "recordptr.hpp" +#include "nifstream.hpp" +#include "nifkey.hpp" + +namespace Nif +{ +/** A record that can have extra data. The extra data objects + themselves descend from the Extra class, and all the extra data + connected to an object form a linked list +*/ +class Extra : public Record +{ +public: + ExtraPtr extra; + + void read(NIFStream *nif) { extra.read(nif); } + void post(NIFFile *nif) { extra.post(nif); } +}; + +class Controller : public Record +{ +public: + ControllerPtr next; + int flags; + float frequency, phase; + float timeStart, timeStop; + ControlledPtr target; + + void read(NIFStream *nif) + { + next.read(nif); + + flags = nif->getUShort(); + + frequency = nif->getFloat(); + phase = nif->getFloat(); + timeStart = nif->getFloat(); + timeStop = nif->getFloat(); + + target.read(nif); + } + + void post(NIFFile *nif) + { + Record::post(nif); + next.post(nif); + target.post(nif); + } +}; + +/// Anything that has a controller +class Controlled : public Extra +{ +public: + ControllerPtr controller; + + void read(NIFStream *nif) + { + Extra::read(nif); + controller.read(nif); + } + + void post(NIFFile *nif) + { + Extra::post(nif); + controller.post(nif); + } +}; + +/// Has name, extra-data and controller +class Named : public Controlled +{ +public: + std::string name; + + void read(NIFStream *nif) + { + name = nif->getString(); + Controlled::read(nif); + } +}; +typedef Named NiSequenceStreamHelper; + +} // Namespace +#endif diff --git a/components/nif/controlled.hpp b/components/nif/controlled.hpp index 6acb8ff20..815aa7d3f 100644 --- a/components/nif/controlled.hpp +++ b/components/nif/controlled.hpp @@ -24,44 +24,70 @@ #ifndef OPENMW_COMPONENTS_NIF_CONTROLLED_HPP #define OPENMW_COMPONENTS_NIF_CONTROLLED_HPP -#include "extra.hpp" -#include "controller.hpp" +#include "base.hpp" namespace Nif { -/// Anything that has a controller -class Controlled : public Extra +class NiSourceTexture : public Named { public: - ControllerPtr controller; + // Is this an external (references a separate texture file) or + // internal (data is inside the nif itself) texture? + bool external; + + std::string filename; // In case of external textures + NiPixelDataPtr data; // In case of internal textures + + /* Pixel layout + 0 - Palettised + 1 - High color 16 + 2 - True color 32 + 3 - Compressed + 4 - Bumpmap + 5 - Default */ + int pixel; + + /* Mipmap format + 0 - no + 1 - yes + 2 - default */ + int mipmap; + + /* Alpha + 0 - none + 1 - binary + 2 - smooth + 3 - default (use material alpha, or multiply material with texture if present) + */ + int alpha; void read(NIFStream *nif) { - Extra::read(nif); - controller.read(nif); + Named::read(nif); + + external = !!nif->getChar(); + if(external) + filename = nif->getString(); + else + { + nif->getChar(); // always 1 + data.read(nif); + } + + pixel = nif->getInt(); + mipmap = nif->getInt(); + alpha = nif->getInt(); + + nif->getChar(); // always 1 } void post(NIFFile *nif) { - Extra::post(nif); - controller.post(nif); - } -}; - -/// Has name, extra-data and controller -class Named : public Controlled -{ -public: - std::string name; - - void read(NIFStream *nif) - { - name = nif->getString(); - Controlled::read(nif); + Named::post(nif); + data.post(nif); } }; -typedef Named NiSequenceStreamHelper; class NiParticleGrowFade : public Controlled { @@ -147,5 +173,7 @@ public: } }; + + } // Namespace #endif diff --git a/components/nif/controller.hpp b/components/nif/controller.hpp index 920e634e6..9ae527e5a 100644 --- a/components/nif/controller.hpp +++ b/components/nif/controller.hpp @@ -24,44 +24,11 @@ #ifndef OPENMW_COMPONENTS_NIF_CONTROLLER_HPP #define OPENMW_COMPONENTS_NIF_CONTROLLER_HPP -#include "record.hpp" -#include "niffile.hpp" -#include "recordptr.hpp" +#include "base.hpp" namespace Nif { -class Controller : public Record -{ -public: - ControllerPtr next; - int flags; - float frequency, phase; - float timeStart, timeStop; - ControlledPtr target; - - void read(NIFStream *nif) - { - next.read(nif); - - flags = nif->getUShort(); - - frequency = nif->getFloat(); - phase = nif->getFloat(); - timeStart = nif->getFloat(); - timeStop = nif->getFloat(); - - target.read(nif); - } - - void post(NIFFile *nif) - { - Record::post(nif); - next.post(nif); - target.post(nif); - } -}; - class NiParticleSystemController : public Controller { public: diff --git a/components/nif/data.cpp b/components/nif/data.cpp new file mode 100644 index 000000000..4248b93d2 --- /dev/null +++ b/components/nif/data.cpp @@ -0,0 +1,29 @@ +#include "data.hpp" +#include "node.hpp" + +namespace Nif +{ +void NiSkinInstance::post(NIFFile *nif) +{ + data.post(nif); + root.post(nif); + bones.post(nif); + + if(data.empty() || root.empty()) + nif->fail("NiSkinInstance missing root or data"); + + size_t bnum = bones.length(); + if(bnum != data->bones.size()) + nif->fail("Mismatch in NiSkinData bone count"); + + root->makeRootBone(&data->trafo); + + for(size_t i=0; ifail("Oops: Missing bone! Don't know how to handle this."); + bones[i]->makeBone(i, data->bones[i]); + } +} + +} // Namespace diff --git a/components/nif/data.hpp b/components/nif/data.hpp index e94388768..d9de12fb5 100644 --- a/components/nif/data.hpp +++ b/components/nif/data.hpp @@ -24,74 +24,11 @@ #ifndef OPENMW_COMPONENTS_NIF_DATA_HPP #define OPENMW_COMPONENTS_NIF_DATA_HPP -#include "controlled.hpp" - -#include -#include +#include "base.hpp" namespace Nif { -class NiSourceTexture : public Named -{ -public: - // Is this an external (references a separate texture file) or - // internal (data is inside the nif itself) texture? - bool external; - - std::string filename; // In case of external textures - NiPixelDataPtr data; // In case of internal textures - - /* Pixel layout - 0 - Palettised - 1 - High color 16 - 2 - True color 32 - 3 - Compressed - 4 - Bumpmap - 5 - Default */ - int pixel; - - /* Mipmap format - 0 - no - 1 - yes - 2 - default */ - int mipmap; - - /* Alpha - 0 - none - 1 - binary - 2 - smooth - 3 - default (use material alpha, or multiply material with texture if present) - */ - int alpha; - - void read(NIFStream *nif) - { - Named::read(nif); - - external = !!nif->getChar(); - if(external) - filename = nif->getString(); - else - { - nif->getChar(); // always 1 - data.read(nif); - } - - pixel = nif->getInt(); - mipmap = nif->getInt(); - alpha = nif->getInt(); - - nif->getChar(); // always 1 - } - - void post(NIFFile *nif) - { - Named::post(nif); - data.post(nif); - } -}; - // Common ancestor for several data classes class ShapeData : public Record { @@ -211,7 +148,7 @@ public: class NiPosData : public Record { public: - Vector3KeyList mKeyList; + Vector3KeyMap mKeyList; void read(NIFStream *nif) { @@ -222,7 +159,7 @@ public: class NiUVData : public Record { public: - FloatKeyList mKeyList[4]; + FloatKeyMap mKeyList[4]; void read(NIFStream *nif) { @@ -234,7 +171,7 @@ public: class NiFloatData : public Record { public: - FloatKeyList mKeyList; + FloatKeyMap mKeyList; void read(NIFStream *nif) { @@ -284,11 +221,11 @@ public: class NiColorData : public Record { public: - Vector4KeyList mKeyList; + Vector4KeyMap mKeyMap; void read(NIFStream *nif) { - mKeyList.read(nif); + mKeyMap.read(nif); } }; @@ -389,7 +326,7 @@ public: struct NiMorphData : public Record { struct MorphData { - FloatKeyList mData; + FloatKeyMap mData; std::vector mVertices; }; std::vector mMorphs; @@ -412,11 +349,14 @@ struct NiMorphData : public Record struct NiKeyframeData : public Record { - QuaternionKeyList mRotations; - //\FIXME mXYZ_Keys are read, but not used. - FloatKeyList mXYZ_Keys; - Vector3KeyList mTranslations; - FloatKeyList mScales; + QuaternionKeyMap mRotations; + + FloatKeyMap mXRotations; + FloatKeyMap mYRotations; + FloatKeyMap mZRotations; + + Vector3KeyMap mTranslations; + FloatKeyMap mScales; void read(NIFStream *nif) { @@ -425,12 +365,9 @@ struct NiKeyframeData : public Record { //Chomp unused float nif->getFloat(); - for(size_t i=0;i<3;++i) - { - //Read concatenates items together. - mXYZ_Keys.read(nif,true); - } - nif->file->warn("XYZ_ROTATION_KEY read, but not used!"); + mXRotations.read(nif, true); + mYRotations.read(nif, true); + mZRotations.read(nif, true); } mTranslations.read(nif); mScales.read(nif); diff --git a/components/nif/extra.hpp b/components/nif/extra.hpp index 45c4fefc6..2913c62b0 100644 --- a/components/nif/extra.hpp +++ b/components/nif/extra.hpp @@ -24,26 +24,11 @@ #ifndef OPENMW_COMPONENTS_NIF_EXTRA_HPP #define OPENMW_COMPONENTS_NIF_EXTRA_HPP -#include "record.hpp" -#include "niffile.hpp" -#include "recordptr.hpp" +#include "base.hpp" namespace Nif { -/** A record that can have extra data. The extra data objects - themselves decend from the Extra class, and all the extra data - connected to an object form a linked list -*/ -class Extra : public Record -{ -public: - ExtraPtr extra; - - void read(NIFStream *nif) { extra.read(nif); } - void post(NIFFile *nif) { extra.post(nif); } -}; - class NiVertWeightsExtraData : public Extra { public: diff --git a/components/nif/niffile.cpp b/components/nif/niffile.cpp index 84f4aacee..4f3ee95cb 100644 --- a/components/nif/niffile.cpp +++ b/components/nif/niffile.cpp @@ -1,177 +1,15 @@ -/* - OpenMW - The completely unofficial reimplementation of Morrowind - Copyright (C) 2008-2010 Nicolay Korslund - Email: < korslund@gmail.com > - WWW: http://openmw.sourceforge.net/ - - This file (nif_file.cpp) is part of the OpenMW package. - - OpenMW is distributed as free software: you can redistribute it - and/or modify it under the terms of the GNU General Public License - version 3, as published by the Free Software Foundation. - - This program is distributed in the hope that it will be useful, but - WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - General Public License for more details. - - You should have received a copy of the GNU General Public License - version 3 along with this program. If not, see - http://www.gnu.org/licenses/ . - - */ - #include "niffile.hpp" -#include "record.hpp" -#include "components/misc/stringops.hpp" - -#include "extra.hpp" -#include "controlled.hpp" -#include "node.hpp" -#include "property.hpp" -#include "data.hpp" #include "effect.hpp" -#include "controller.hpp" -#include +#include -//TODO: when threading is needed, enable these -//#include -#include +#include namespace Nif { -class NIFFile::LoadedCache -{ - //TODO: enable this to make cache thread safe... - //typedef boost::mutex mutex; - - struct mutex - { - void lock () {}; - void unlock () {} - }; - - typedef boost::lock_guard lock_guard; - typedef std::map < std::string, boost::weak_ptr > loaded_map; - typedef std::vector < boost::shared_ptr > locked_files; - - static int sLockLevel; - static mutex sProtector; - static loaded_map sLoadedMap; - static locked_files sLockedFiles; - -public: - - static ptr create (const std::string &name) - { - lock_guard _ (sProtector); - - ptr result; - - // lookup the resource - loaded_map::iterator i = sLoadedMap.find (name); - - if (i == sLoadedMap.end ()) // it doesn't existing currently, - { // or hasn't in the very near past - - // create it now, for smoother threading if needed, the - // loading should be performed outside of the sLoaderMap - // lock and an alternate mechanism should be used to - // synchronize threads competing to load the same resource - result = boost::make_shared (name, psudo_private_modifier()); - - // if we are locking the cache add an extra reference - // to keep the file in memory - if (sLockLevel > 0) - sLockedFiles.push_back (result); - - // stash a reference to the resource so that future - // calls can benefit - sLoadedMap [name] = boost::weak_ptr (result); - } - else // it may (probably) still exists - { - // attempt to get the reference - result = i->second.lock (); - - if (!result) // resource is in the process of being destroyed - { - // create a new instance, to replace the one that has - // begun the irreversible process of being destroyed - result = boost::make_shared (name, psudo_private_modifier()); - - // respect the cache lock... - if (sLockLevel > 0) - sLockedFiles.push_back (result); - - // we potentially overwrite an expired pointer here - // but the other thread performing the delete on - // the previous copy of this resource will detect it - // and make sure not to erase the new reference - sLoadedMap [name] = boost::weak_ptr (result); - } - } - - // we made it! - return result; - } - - static void release (NIFFile * file) - { - lock_guard _ (sProtector); - - loaded_map::iterator i = sLoadedMap.find (file->filename); - - // its got to be in here, it just might not be us... - assert (i != sLoadedMap.end ()); - - // if weak_ptr is still expired, this resource hasn't been recreated - // between the initiation of the final release due to destruction - // of the last shared pointer and this thread acquiring the lock on - // the loader map - if (i->second.expired ()) - sLoadedMap.erase (i); - } - - static void lockCache () - { - lock_guard _ (sProtector); - - sLockLevel++; - } - - static void unlockCache () - { - locked_files resetList; - - { - lock_guard _ (sProtector); - - if (--sLockLevel) - sLockedFiles.swap(resetList); - } - - // this not necessary, but makes it clear that the - // deletion of the locked cache entries is being done - // outside the protection of sProtector - resetList.clear (); - } -}; - -int NIFFile::LoadedCache::sLockLevel = 0; -NIFFile::LoadedCache::mutex NIFFile::LoadedCache::sProtector; -NIFFile::LoadedCache::loaded_map NIFFile::LoadedCache::sLoadedMap; -NIFFile::LoadedCache::locked_files NIFFile::LoadedCache::sLockedFiles; - -// these three calls are forwarded to the cache implementation... -void NIFFile::lockCache () { LoadedCache::lockCache (); } -void NIFFile::unlockCache () { LoadedCache::unlockCache (); } -NIFFile::ptr NIFFile::create (const std::string &name) { return LoadedCache::create (name); } - /// Open a NIF stream. The name is used for error messages. -NIFFile::NIFFile(const std::string &name, psudo_private_modifier) +NIFFile::NIFFile(const std::string &name) : ver(0) , filename(name) { @@ -180,10 +18,10 @@ NIFFile::NIFFile(const std::string &name, psudo_private_modifier) NIFFile::~NIFFile() { - LoadedCache::release (this); - - for(std::size_t i=0; i::iterator it = records.begin() ; it != records.end(); ++it) + { + delete *it; + } } template static Record* construct() { return new NodeType; } @@ -192,115 +30,116 @@ struct RecordFactoryEntry { typedef Record* (*create_t) (); - char const * mName; create_t mCreate; RecordType mType; }; -/* These are all the record types we know how to read. - - This can be heavily optimized later if needed. For example, a - hash table or a FSM-based parser could be used to look up - node names. -*/ +///Helper function for adding records to the factory map +static std::pair makeEntry(std::string recName, Record* (*create_t) (), RecordType type) +{ + RecordFactoryEntry anEntry = {create_t,type}; + return std::make_pair(recName, anEntry); +} -static const RecordFactoryEntry recordFactories [] = { +///These are all the record types we know how to read. +static std::map makeFactory() +{ + std::map newFactory; + newFactory.insert(makeEntry("NiNode", &construct , RC_NiNode )); + newFactory.insert(makeEntry("AvoidNode", &construct , RC_AvoidNode )); + newFactory.insert(makeEntry("NiBSParticleNode", &construct , RC_NiBSParticleNode )); + newFactory.insert(makeEntry("NiBSAnimationNode", &construct , RC_NiBSAnimationNode )); + newFactory.insert(makeEntry("NiBillboardNode", &construct , RC_NiBillboardNode )); + newFactory.insert(makeEntry("NiTriShape", &construct , RC_NiTriShape )); + newFactory.insert(makeEntry("NiRotatingParticles", &construct , RC_NiRotatingParticles )); + newFactory.insert(makeEntry("NiAutoNormalParticles", &construct , RC_NiAutoNormalParticles )); + newFactory.insert(makeEntry("NiCamera", &construct , RC_NiCamera )); + newFactory.insert(makeEntry("RootCollisionNode", &construct , RC_RootCollisionNode )); + newFactory.insert(makeEntry("NiTexturingProperty", &construct , RC_NiTexturingProperty )); + newFactory.insert(makeEntry("NiFogProperty", &construct , RC_NiFogProperty )); + newFactory.insert(makeEntry("NiMaterialProperty", &construct , RC_NiMaterialProperty )); + newFactory.insert(makeEntry("NiZBufferProperty", &construct , RC_NiZBufferProperty )); + newFactory.insert(makeEntry("NiAlphaProperty", &construct , RC_NiAlphaProperty )); + newFactory.insert(makeEntry("NiVertexColorProperty", &construct , RC_NiVertexColorProperty )); + newFactory.insert(makeEntry("NiShadeProperty", &construct , RC_NiShadeProperty )); + newFactory.insert(makeEntry("NiDitherProperty", &construct , RC_NiDitherProperty )); + newFactory.insert(makeEntry("NiWireframeProperty", &construct , RC_NiWireframeProperty )); + newFactory.insert(makeEntry("NiSpecularProperty", &construct , RC_NiSpecularProperty )); + newFactory.insert(makeEntry("NiStencilProperty", &construct , RC_NiStencilProperty )); + newFactory.insert(makeEntry("NiVisController", &construct , RC_NiVisController )); + newFactory.insert(makeEntry("NiGeomMorpherController", &construct , RC_NiGeomMorpherController )); + newFactory.insert(makeEntry("NiKeyframeController", &construct , RC_NiKeyframeController )); + newFactory.insert(makeEntry("NiAlphaController", &construct , RC_NiAlphaController )); + newFactory.insert(makeEntry("NiUVController", &construct , RC_NiUVController )); + newFactory.insert(makeEntry("NiPathController", &construct , RC_NiPathController )); + newFactory.insert(makeEntry("NiMaterialColorController", &construct , RC_NiMaterialColorController )); + newFactory.insert(makeEntry("NiBSPArrayController", &construct , RC_NiBSPArrayController )); + newFactory.insert(makeEntry("NiParticleSystemController", &construct , RC_NiParticleSystemController )); + newFactory.insert(makeEntry("NiFlipController", &construct , RC_NiFlipController )); + newFactory.insert(makeEntry("NiAmbientLight", &construct , RC_NiLight )); + newFactory.insert(makeEntry("NiDirectionalLight", &construct , RC_NiLight )); + newFactory.insert(makeEntry("NiTextureEffect", &construct , RC_NiTextureEffect )); + newFactory.insert(makeEntry("NiVertWeightsExtraData", &construct , RC_NiVertWeightsExtraData )); + newFactory.insert(makeEntry("NiTextKeyExtraData", &construct , RC_NiTextKeyExtraData )); + newFactory.insert(makeEntry("NiStringExtraData", &construct , RC_NiStringExtraData )); + newFactory.insert(makeEntry("NiGravity", &construct , RC_NiGravity )); + newFactory.insert(makeEntry("NiPlanarCollider", &construct , RC_NiPlanarCollider )); + newFactory.insert(makeEntry("NiParticleGrowFade", &construct , RC_NiParticleGrowFade )); + newFactory.insert(makeEntry("NiParticleColorModifier", &construct , RC_NiParticleColorModifier )); + newFactory.insert(makeEntry("NiParticleRotation", &construct , RC_NiParticleRotation )); + newFactory.insert(makeEntry("NiFloatData", &construct , RC_NiFloatData )); + newFactory.insert(makeEntry("NiTriShapeData", &construct , RC_NiTriShapeData )); + newFactory.insert(makeEntry("NiVisData", &construct , RC_NiVisData )); + newFactory.insert(makeEntry("NiColorData", &construct , RC_NiColorData )); + newFactory.insert(makeEntry("NiPixelData", &construct , RC_NiPixelData )); + newFactory.insert(makeEntry("NiMorphData", &construct , RC_NiMorphData )); + newFactory.insert(makeEntry("NiKeyframeData", &construct , RC_NiKeyframeData )); + newFactory.insert(makeEntry("NiSkinData", &construct , RC_NiSkinData )); + newFactory.insert(makeEntry("NiUVData", &construct , RC_NiUVData )); + newFactory.insert(makeEntry("NiPosData", &construct , RC_NiPosData )); + newFactory.insert(makeEntry("NiRotatingParticlesData", &construct , RC_NiRotatingParticlesData )); + newFactory.insert(makeEntry("NiAutoNormalParticlesData", &construct , RC_NiAutoNormalParticlesData )); + newFactory.insert(makeEntry("NiSequenceStreamHelper", &construct , RC_NiSequenceStreamHelper )); + newFactory.insert(makeEntry("NiSourceTexture", &construct , RC_NiSourceTexture )); + newFactory.insert(makeEntry("NiSkinInstance", &construct , RC_NiSkinInstance )); + return newFactory; +} - { "NiNode", &construct , RC_NiNode }, - { "AvoidNode", &construct , RC_AvoidNode }, - { "NiBSParticleNode", &construct , RC_NiBSParticleNode }, - { "NiBSAnimationNode", &construct , RC_NiBSAnimationNode }, - { "NiBillboardNode", &construct , RC_NiBillboardNode }, - { "NiTriShape", &construct , RC_NiTriShape }, - { "NiRotatingParticles", &construct , RC_NiRotatingParticles }, - { "NiAutoNormalParticles", &construct , RC_NiAutoNormalParticles }, - { "NiCamera", &construct , RC_NiCamera }, - { "RootCollisionNode", &construct , RC_RootCollisionNode }, - { "NiTexturingProperty", &construct , RC_NiTexturingProperty }, - { "NiMaterialProperty", &construct , RC_NiMaterialProperty }, - { "NiZBufferProperty", &construct , RC_NiZBufferProperty }, - { "NiAlphaProperty", &construct , RC_NiAlphaProperty }, - { "NiVertexColorProperty", &construct , RC_NiVertexColorProperty }, - { "NiShadeProperty", &construct , RC_NiShadeProperty }, - { "NiDitherProperty", &construct , RC_NiDitherProperty }, - { "NiWireframeProperty", &construct , RC_NiWireframeProperty }, - { "NiSpecularProperty", &construct , RC_NiSpecularProperty }, - { "NiStencilProperty", &construct , RC_NiStencilProperty }, - { "NiVisController", &construct , RC_NiVisController }, - { "NiGeomMorpherController", &construct , RC_NiGeomMorpherController }, - { "NiKeyframeController", &construct , RC_NiKeyframeController }, - { "NiAlphaController", &construct , RC_NiAlphaController }, - { "NiUVController", &construct , RC_NiUVController }, - { "NiPathController", &construct , RC_NiPathController }, - { "NiMaterialColorController", &construct , RC_NiMaterialColorController }, - { "NiBSPArrayController", &construct , RC_NiBSPArrayController }, - { "NiParticleSystemController", &construct , RC_NiParticleSystemController }, - { "NiFlipController", &construct , RC_NiFlipController }, - { "NiAmbientLight", &construct , RC_NiLight }, - { "NiDirectionalLight", &construct , RC_NiLight }, - { "NiTextureEffect", &construct , RC_NiTextureEffect }, - { "NiVertWeightsExtraData", &construct , RC_NiVertWeightsExtraData }, - { "NiTextKeyExtraData", &construct , RC_NiTextKeyExtraData }, - { "NiStringExtraData", &construct , RC_NiStringExtraData }, - { "NiGravity", &construct , RC_NiGravity }, - { "NiPlanarCollider", &construct , RC_NiPlanarCollider }, - { "NiParticleGrowFade", &construct , RC_NiParticleGrowFade }, - { "NiParticleColorModifier", &construct , RC_NiParticleColorModifier }, - { "NiParticleRotation", &construct , RC_NiParticleRotation }, - { "NiFloatData", &construct , RC_NiFloatData }, - { "NiTriShapeData", &construct , RC_NiTriShapeData }, - { "NiVisData", &construct , RC_NiVisData }, - { "NiColorData", &construct , RC_NiColorData }, - { "NiPixelData", &construct , RC_NiPixelData }, - { "NiMorphData", &construct , RC_NiMorphData }, - { "NiKeyframeData", &construct , RC_NiKeyframeData }, - { "NiSkinData", &construct , RC_NiSkinData }, - { "NiUVData", &construct , RC_NiUVData }, - { "NiPosData", &construct , RC_NiPosData }, - { "NiRotatingParticlesData", &construct , RC_NiRotatingParticlesData }, - { "NiAutoNormalParticlesData", &construct , RC_NiAutoNormalParticlesData }, - { "NiSequenceStreamHelper", &construct , RC_NiSequenceStreamHelper }, - { "NiSourceTexture", &construct , RC_NiSourceTexture }, - { "NiSkinInstance", &construct , RC_NiSkinInstance }, -}; -static RecordFactoryEntry const * recordFactories_begin = &recordFactories [0]; -static RecordFactoryEntry const * recordFactories_end = &recordFactories [sizeof (recordFactories) / sizeof (recordFactories[0])]; +///Make the factory map used for parsing the file +static const std::map factories = makeFactory(); -RecordFactoryEntry const * lookupRecordFactory (char const * name) +/// Get the file's version in a human readable form +std::string NIFFile::printVersion(unsigned int version) { - RecordFactoryEntry const * i; - - for (i = recordFactories_begin; i != recordFactories_end; ++i) - if (strcmp (name, i->mName) == 0) - break; + union ver_quad + { + uint32_t full; + uint8_t quad[4]; + } version_out; - if (i == recordFactories_end) - return NULL; + version_out.full = version; - return i; + return Ogre::StringConverter::toString(version_out.quad[3]) + +"." + Ogre::StringConverter::toString(version_out.quad[2]) + +"." + Ogre::StringConverter::toString(version_out.quad[1]) + +"." + Ogre::StringConverter::toString(version_out.quad[0]); } -/* This file implements functions from the NIFFile class. It is also - where we stash all the functions we couldn't add as inline - definitions in the record types. - */ - void NIFFile::parse() { NIFStream nif (this, Ogre::ResourceGroupManager::getSingleton().openResource(filename)); // Check the header string - std::string head = nif.getString(40); + std::string head = nif.getVersionString(); if(head.compare(0, 22, "NetImmerse File Format") != 0) - fail("Invalid NIF header"); + fail("Invalid NIF header: " + head); // Get BCD version - ver = nif.getInt(); + ver = nif.getUInt(); if(ver != VER_MW) - fail("Unsupported NIF version"); - + fail("Unsupported NIF version: " + printVersion(ver)); // Number of records size_t recNum = nif.getInt(); records.resize(recNum); @@ -322,12 +161,13 @@ void NIFFile::parse() if(rec.empty()) fail("Record number " + Ogre::StringConverter::toString(i) + " out of " + Ogre::StringConverter::toString(recNum) + " is blank."); - RecordFactoryEntry const * entry = lookupRecordFactory (rec.c_str ()); - if (entry != NULL) + std::map::const_iterator entry = factories.find(rec); + + if (entry != factories.end()) { - r = entry->mCreate (); - r->recType = entry->mType; + r = entry->second.mCreate (); + r->recType = entry->second.mType; } else fail("Unknown record type " + rec); @@ -338,24 +178,24 @@ void NIFFile::parse() r->recIndex = i; records[i] = r; r->read(&nif); - - // Discard tranformations for the root node, otherwise some meshes - // occasionally get wrong orientation. Only for NiNode-s for now, but - // can be expanded if needed. - // This should be rewritten when the method is cleaned up. - if (0 == i && rec == "NiNode") - { - static_cast(r)->trafo = Nif::Transformation::getIdentity(); - } } size_t rootNum = nif.getUInt(); roots.resize(rootNum); + //Determine which records are roots for(size_t i = 0;i < rootNum;i++) { - intptr_t idx = nif.getInt(); - roots[i] = ((idx >= 0) ? records.at(idx) : NULL); + int idx = nif.getInt(); + if (idx >= 0) + { + roots[i] = records.at(idx); + } + else + { + roots[i] = NULL; + warn("Null Root found"); + } } // Once parsing is done, do post-processing. @@ -363,81 +203,4 @@ void NIFFile::parse() records[i]->post(this); } -/// \todo move to the write cpp file - -void NiSkinInstance::post(NIFFile *nif) -{ - data.post(nif); - root.post(nif); - bones.post(nif); - - if(data.empty() || root.empty()) - nif->fail("NiSkinInstance missing root or data"); - - size_t bnum = bones.length(); - if(bnum != data->bones.size()) - nif->fail("Mismatch in NiSkinData bone count"); - - root->makeRootBone(&data->trafo); - - for(size_t i=0; ifail("Oops: Missing bone! Don't know how to handle this."); - bones[i]->makeBone(i, data->bones[i]); - } -} - - -void Node::getProperties(const Nif::NiTexturingProperty *&texprop, - const Nif::NiMaterialProperty *&matprop, - const Nif::NiAlphaProperty *&alphaprop, - const Nif::NiVertexColorProperty *&vertprop, - const Nif::NiZBufferProperty *&zprop, - const Nif::NiSpecularProperty *&specprop, - const Nif::NiWireframeProperty *&wireprop) const -{ - if(parent) - parent->getProperties(texprop, matprop, alphaprop, vertprop, zprop, specprop, wireprop); - - for(size_t i = 0;i < props.length();i++) - { - // Entries may be empty - if(props[i].empty()) - continue; - - const Nif::Property *pr = props[i].getPtr(); - if(pr->recType == Nif::RC_NiTexturingProperty) - texprop = static_cast(pr); - else if(pr->recType == Nif::RC_NiMaterialProperty) - matprop = static_cast(pr); - else if(pr->recType == Nif::RC_NiAlphaProperty) - alphaprop = static_cast(pr); - else if(pr->recType == Nif::RC_NiVertexColorProperty) - vertprop = static_cast(pr); - else if(pr->recType == Nif::RC_NiZBufferProperty) - zprop = static_cast(pr); - else if(pr->recType == Nif::RC_NiSpecularProperty) - specprop = static_cast(pr); - else if(pr->recType == Nif::RC_NiWireframeProperty) - wireprop = static_cast(pr); - else - std::cerr<< "Unhandled property type: "<recName <getWorldTransform() * getLocalTransform(); - return getLocalTransform(); -} - } diff --git a/components/nif/niffile.hpp b/components/nif/niffile.hpp index d70124263..ceb9984fb 100644 --- a/components/nif/niffile.hpp +++ b/components/nif/niffile.hpp @@ -1,53 +1,13 @@ -/* - OpenMW - The completely unofficial reimplementation of Morrowind - Copyright (C) 2008-2010 Nicolay Korslund - Email: < korslund@gmail.com > - WWW: http://openmw.sourceforge.net/ - - This file (nif_file.h) is part of the OpenMW package. - - OpenMW is distributed as free software: you can redistribute it - and/or modify it under the terms of the GNU General Public License - version 3, as published by the Free Software Foundation. - - This program is distributed in the hope that it will be useful, but - WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - General Public License for more details. - - You should have received a copy of the GNU General Public License - version 3 along with this program. If not, see - http://www.gnu.org/licenses/ . - - */ +///Main header for reading .nif files #ifndef OPENMW_COMPONENTS_NIF_NIFFILE_HPP #define OPENMW_COMPONENTS_NIF_NIFFILE_HPP -#include -#include -#include -#include -#include -#include -#include -#include - #include #include -#include -#include - -#include -#include -#include -#include - -#include +#include #include "record.hpp" -#include "niftypes.hpp" -#include "nifstream.hpp" namespace Nif { @@ -59,64 +19,52 @@ class NIFFile }; /// Nif file version - int ver; + unsigned int ver; - /// File name, used for error messages + /// File name, used for error messages and opening the file std::string filename; /// Record list std::vector records; - /// Root list + /// Root list. This is a select portion of the pointers from records std::vector roots; /// Parse the file void parse(); - class LoadedCache; - friend class LoadedCache; + /// Get the file's version in a human readable form + ///\returns A string containing a human readable NIF version number + std::string printVersion(unsigned int version); - // attempt to protect NIFFile from misuse... - struct psudo_private_modifier {}; // this dirty little trick should optimize out + ///Private Copy Constructor NIFFile (NIFFile const &); + ///\overload void operator = (NIFFile const &); public: - /// Used for error handling + /// Used if file parsing fails void fail(const std::string &msg) { - std::string err = "NIFFile Error: " + msg; + std::string err = " NIFFile Error: " + msg; err += "\nFile: " + filename; throw std::runtime_error(err); } - + /// Used when something goes wrong, but not catastrophically so void warn(const std::string &msg) { - std::cerr << "NIFFile Warning: " << msg < ptr; - - /// Open a NIF stream. The name is used for error messages. - NIFFile(const std::string &name, psudo_private_modifier); + /// Open a NIF stream. The name is used for error messages and opening the file. + NIFFile(const std::string &name); ~NIFFile(); - static ptr create (const std::string &name); - static void lockCache (); - static void unlockCache (); - - struct CacheLock - { - CacheLock () { lockCache (); } - ~CacheLock () { unlockCache (); } - }; - /// Get a given record Record *getRecord(size_t index) const { Record *res = records.at(index); - assert(res != NULL); return res; } /// Number of records @@ -126,139 +74,16 @@ public: Record *getRoot(size_t index=0) const { Record *res = roots.at(index); - assert(res != NULL); return res; } /// Number of roots size_t numRoots() const { return roots.size(); } -}; - -template -struct KeyT { - float mTime; - T mValue; - T mForwardValue; // Only for Quadratic interpolation, and never for QuaternionKeyList - T mBackwardValue; // Only for Quadratic interpolation, and never for QuaternionKeyList - float mTension; // Only for TBC interpolation - float mBias; // Only for TBC interpolation - float mContinuity; // Only for TBC interpolation + /// Get the name of the file + std::string getFilename(){ return filename; } }; -typedef KeyT FloatKey; -typedef KeyT Vector3Key; -typedef KeyT Vector4Key; -typedef KeyT QuaternionKey; - -template -struct KeyListT { - typedef std::vector< KeyT > VecType; - - static const unsigned int sLinearInterpolation = 1; - static const unsigned int sQuadraticInterpolation = 2; - static const unsigned int sTBCInterpolation = 3; - static const unsigned int sXYZInterpolation = 4; - - unsigned int mInterpolationType; - VecType mKeys; - - KeyListT() : mInterpolationType(sLinearInterpolation) {} - //Read in a KeyGroup (see http://niftools.sourceforge.net/doc/nif/NiKeyframeData.html) - void read(NIFStream *nif, bool force=false) - { - assert(nif); - - mInterpolationType = 0; - - size_t count = nif->getUInt(); - if(count == 0 && !force) - return; - - //If we aren't forcing things, make sure that read clears any previous keys - if(!force) - mKeys.clear(); - - mInterpolationType = nif->getUInt(); - - KeyT key; - NIFStream &nifReference = *nif; - - if(mInterpolationType == sLinearInterpolation) - { - for(size_t i = 0;i < count;i++) - { - readTimeAndValue(nifReference, key); - mKeys.push_back(key); - } - } - else if(mInterpolationType == sQuadraticInterpolation) - { - for(size_t i = 0;i < count;i++) - { - readQuadratic(nifReference, key); - mKeys.push_back(key); - } - } - else if(mInterpolationType == sTBCInterpolation) - { - for(size_t i = 0;i < count;i++) - { - readTBC(nifReference, key); - mKeys.push_back(key); - } - } - //XYZ keys aren't actually read here. - //data.hpp sees that the last type read was sXYZInterpolation and: - // Eats a floating point number, then - // Re-runs the read function 3 more times, with force enabled so that the previous values aren't cleared. - // When it does that it's reading in a bunch of sLinearInterpolation keys, not sXYZInterpolation. - else if(mInterpolationType == sXYZInterpolation) - { - //Don't try to read XYZ keys into the wrong part - if ( count != 1 ) - nif->file->fail("XYZ_ROTATION_KEY count should always be '1' . Retrieved Value: "+Ogre::StringConverter::toString(count)); - } - else if (0 == mInterpolationType) - { - if (count != 0) - nif->file->fail("Interpolation type 0 doesn't work with keys"); - } - else - nif->file->fail("Unhandled interpolation type: "+Ogre::StringConverter::toString(mInterpolationType)); - } - -private: - static void readTimeAndValue(NIFStream &nif, KeyT &key) - { - key.mTime = nif.getFloat(); - key.mValue = (nif.*getValue)(); - } - - static void readQuadratic(NIFStream &nif, KeyT &key) - { - readTimeAndValue(nif, key); - } - - template - static void readQuadratic(NIFStream &nif, KeyT &key) - { - readTimeAndValue(nif, key); - key.mForwardValue = (nif.*getValue)(); - key.mBackwardValue = (nif.*getValue)(); - } - static void readTBC(NIFStream &nif, KeyT &key) - { - readTimeAndValue(nif, key); - key.mTension = nif.getFloat(); - key.mBias = nif.getFloat(); - key.mContinuity = nif.getFloat(); - } -}; -typedef KeyListT FloatKeyList; -typedef KeyListT Vector3KeyList; -typedef KeyListT Vector4KeyList; -typedef KeyListT QuaternionKeyList; } // Namespace #endif diff --git a/components/nif/nifkey.hpp b/components/nif/nifkey.hpp new file mode 100644 index 000000000..cb8142720 --- /dev/null +++ b/components/nif/nifkey.hpp @@ -0,0 +1,139 @@ +///File to handle keys used by nif file records + +#ifndef OPENMW_COMPONENTS_NIF_NIFKEY_HPP +#define OPENMW_COMPONENTS_NIF_NIFKEY_HPP + +#include + +#include "nifstream.hpp" + +namespace Nif +{ + +template +struct KeyT { + T mValue; + T mForwardValue; // Only for Quadratic interpolation, and never for QuaternionKeyList + T mBackwardValue; // Only for Quadratic interpolation, and never for QuaternionKeyList + float mTension; // Only for TBC interpolation + float mBias; // Only for TBC interpolation + float mContinuity; // Only for TBC interpolation +}; +typedef KeyT FloatKey; +typedef KeyT Vector3Key; +typedef KeyT Vector4Key; +typedef KeyT QuaternionKey; + +template +struct KeyMapT { + typedef std::map< float, KeyT > MapType; + + static const unsigned int sLinearInterpolation = 1; + static const unsigned int sQuadraticInterpolation = 2; + static const unsigned int sTBCInterpolation = 3; + static const unsigned int sXYZInterpolation = 4; + + unsigned int mInterpolationType; + MapType mKeys; + + KeyMapT() : mInterpolationType(sLinearInterpolation) {} + + //Read in a KeyGroup (see http://niftools.sourceforge.net/doc/nif/NiKeyframeData.html) + void read(NIFStream *nif, bool force=false) + { + assert(nif); + + mInterpolationType = 0; + + size_t count = nif->getUInt(); + if(count == 0 && !force) + return; + + mKeys.clear(); + + mInterpolationType = nif->getUInt(); + + KeyT key; + NIFStream &nifReference = *nif; + + if(mInterpolationType == sLinearInterpolation) + { + for(size_t i = 0;i < count;i++) + { + float time = nif->getFloat(); + readValue(nifReference, key); + mKeys[time] = key; + } + } + else if(mInterpolationType == sQuadraticInterpolation) + { + for(size_t i = 0;i < count;i++) + { + float time = nif->getFloat(); + readQuadratic(nifReference, key); + mKeys[time] = key; + } + } + else if(mInterpolationType == sTBCInterpolation) + { + for(size_t i = 0;i < count;i++) + { + float time = nif->getFloat(); + readTBC(nifReference, key); + mKeys[time] = key; + } + } + //XYZ keys aren't actually read here. + //data.hpp sees that the last type read was sXYZInterpolation and: + // Eats a floating point number, then + // Re-runs the read function 3 more times. + // When it does that it's reading in a bunch of sLinearInterpolation keys, not sXYZInterpolation. + else if(mInterpolationType == sXYZInterpolation) + { + //Don't try to read XYZ keys into the wrong part + if ( count != 1 ) + nif->file->fail("XYZ_ROTATION_KEY count should always be '1' . Retrieved Value: "+Ogre::StringConverter::toString(count)); + } + else if (0 == mInterpolationType) + { + if (count != 0) + nif->file->fail("Interpolation type 0 doesn't work with keys"); + } + else + nif->file->fail("Unhandled interpolation type: "+Ogre::StringConverter::toString(mInterpolationType)); + } + +private: + static void readValue(NIFStream &nif, KeyT &key) + { + key.mValue = (nif.*getValue)(); + } + + static void readQuadratic(NIFStream &nif, KeyT &key) + { + readValue(nif, key); + } + + template + static void readQuadratic(NIFStream &nif, KeyT &key) + { + readValue(nif, key); + key.mForwardValue = (nif.*getValue)(); + key.mBackwardValue = (nif.*getValue)(); + } + + static void readTBC(NIFStream &nif, KeyT &key) + { + readValue(nif, key); + key.mTension = nif.getFloat(); + key.mBias = nif.getFloat(); + key.mContinuity = nif.getFloat(); + } +}; +typedef KeyMapT FloatKeyMap; +typedef KeyMapT Vector3KeyMap; +typedef KeyMapT Vector4KeyMap; +typedef KeyMapT QuaternionKeyMap; + +} // Namespace +#endif //#ifndef OPENMW_COMPONENTS_NIF_NIFKEY_HPP diff --git a/components/nif/nifstream.cpp b/components/nif/nifstream.cpp new file mode 100644 index 000000000..e5699db7b --- /dev/null +++ b/components/nif/nifstream.cpp @@ -0,0 +1,146 @@ +#include "nifstream.hpp" +//For error reporting +#include "niffile.hpp" + +namespace Nif +{ + +//Private functions +uint8_t NIFStream::read_byte() +{ + uint8_t byte; + if(inp->read(&byte, 1) != 1) return 0; + return byte; +} +uint16_t NIFStream::read_le16() +{ + uint8_t buffer[2]; + if(inp->read(buffer, 2) != 2) return 0; + return buffer[0] | (buffer[1]<<8); +} +uint32_t NIFStream::read_le32() +{ + uint8_t buffer[4]; + if(inp->read(buffer, 4) != 4) return 0; + return buffer[0] | (buffer[1]<<8) | (buffer[2]<<16) | (buffer[3]<<24); +} +float NIFStream::read_le32f() +{ + union { + uint32_t i; + float f; + } u = { read_le32() }; + return u.f; +} + +//Public functions +Ogre::Vector2 NIFStream::getVector2() +{ + float a[2]; + for(size_t i = 0;i < 2;i++) + a[i] = getFloat(); + return Ogre::Vector2(a); +} +Ogre::Vector3 NIFStream::getVector3() +{ + float a[3]; + for(size_t i = 0;i < 3;i++) + a[i] = getFloat(); + return Ogre::Vector3(a); +} +Ogre::Vector4 NIFStream::getVector4() +{ + float a[4]; + for(size_t i = 0;i < 4;i++) + a[i] = getFloat(); + return Ogre::Vector4(a); +} +Ogre::Matrix3 NIFStream::getMatrix3() +{ + Ogre::Real a[3][3]; + for(size_t i = 0;i < 3;i++) + { + for(size_t j = 0;j < 3;j++) + a[i][j] = Ogre::Real(getFloat()); + } + return Ogre::Matrix3(a); +} +Ogre::Quaternion NIFStream::getQuaternion() +{ + float a[4]; + for(size_t i = 0;i < 4;i++) + a[i] = getFloat(); + return Ogre::Quaternion(a); +} +Transformation NIFStream::getTrafo() +{ + Transformation t; + t.pos = getVector3(); + t.rotation = getMatrix3(); + t.scale = getFloat(); + return t; +} + +std::string NIFStream::getString(size_t length) +{ + //Make sure we're not reading in too large of a string + unsigned int fileSize = inp->size(); + if(fileSize != 0 && fileSize < length) + file->fail("Attempted to read a string with " + Ogre::StringConverter::toString(length) + " characters , but file is only "+Ogre::StringConverter::toString(fileSize)+ " bytes!"); + + std::vector str (length+1, 0); + + if(inp->read(&str[0], length) != length) + throw std::runtime_error (": String length in NIF file "+ file->getFilename() +" does not match! Expected length: " + + Ogre::StringConverter::toString(length)); + + return &str[0]; +} +std::string NIFStream::getString() +{ + size_t size = read_le32(); + return getString(size); +} +std::string NIFStream::getVersionString() +{ + return inp->getLine(); +} + +void NIFStream::getShorts(std::vector &vec, size_t size) +{ + vec.resize(size); + for(size_t i = 0;i < vec.size();i++) + vec[i] = getShort(); +} +void NIFStream::getFloats(std::vector &vec, size_t size) +{ + vec.resize(size); + for(size_t i = 0;i < vec.size();i++) + vec[i] = getFloat(); +} +void NIFStream::getVector2s(std::vector &vec, size_t size) +{ + vec.resize(size); + for(size_t i = 0;i < vec.size();i++) + vec[i] = getVector2(); +} +void NIFStream::getVector3s(std::vector &vec, size_t size) +{ + vec.resize(size); + for(size_t i = 0;i < vec.size();i++) + vec[i] = getVector3(); +} +void NIFStream::getVector4s(std::vector &vec, size_t size) +{ + vec.resize(size); + for(size_t i = 0;i < vec.size();i++) + vec[i] = getVector4(); +} +void NIFStream::getQuaternions(std::vector &quat, size_t size) +{ + quat.resize(size); + for(size_t i = 0;i < quat.size();i++) + quat[i] = getQuaternion(); +} + +} diff --git a/components/nif/nifstream.hpp b/components/nif/nifstream.hpp index a2595d17b..6c5e83eeb 100644 --- a/components/nif/nifstream.hpp +++ b/components/nif/nifstream.hpp @@ -1,6 +1,21 @@ +///Functions used to read raw binary data from .nif files + #ifndef OPENMW_COMPONENTS_NIF_NIFSTREAM_HPP #define OPENMW_COMPONENTS_NIF_NIFSTREAM_HPP +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "niftypes.hpp" + namespace Nif { @@ -11,32 +26,10 @@ class NIFStream { /// Input stream Ogre::DataStreamPtr inp; - uint8_t read_byte() - { - uint8_t byte; - if(inp->read(&byte, 1) != 1) return 0; - return byte; - } - uint16_t read_le16() - { - uint8_t buffer[2]; - if(inp->read(buffer, 2) != 2) return 0; - return buffer[0] | (buffer[1]<<8); - } - uint32_t read_le32() - { - uint8_t buffer[4]; - if(inp->read(buffer, 4) != 4) return 0; - return buffer[0] | (buffer[1]<<8) | (buffer[2]<<16) | (buffer[3]<<24); - } - float read_le32f() - { - union { - uint32_t i; - float f; - } u = { read_le32() }; - return u.f; - } + uint8_t read_byte(); + uint16_t read_le16(); + uint32_t read_le32(); + float read_le32f(); public: @@ -44,136 +37,35 @@ public: NIFStream (NIFFile * file, Ogre::DataStreamPtr inp): file (file), inp (inp) {} - /************************************************* - Parser functions - ****************************************************/ - - template - struct GetHandler - { - typedef T (NIFStream::*fn_t)(); - - static const fn_t sValue; // this is specialized per supported type in the .cpp file - - static T read (NIFStream* nif) - { - return (nif->*sValue) (); - } - }; - - template - void read (NIFStream* nif, T & Value) - { - Value = GetHandler ::read (nif); - } - void skip(size_t size) { inp->skip(size); } - void read (void * data, size_t size) { inp->read (data, size); } char getChar() { return read_byte(); } short getShort() { return read_le16(); } unsigned short getUShort() { return read_le16(); } int getInt() { return read_le32(); } - int getUInt() { return read_le32(); } + unsigned int getUInt() { return read_le32(); } float getFloat() { return read_le32f(); } - Ogre::Vector2 getVector2() - { - float a[2]; - for(size_t i = 0;i < 2;i++) - a[i] = getFloat(); - return Ogre::Vector2(a); - } - Ogre::Vector3 getVector3() - { - float a[3]; - for(size_t i = 0;i < 3;i++) - a[i] = getFloat(); - return Ogre::Vector3(a); - } - Ogre::Vector4 getVector4() - { - float a[4]; - for(size_t i = 0;i < 4;i++) - a[i] = getFloat(); - return Ogre::Vector4(a); - } - Ogre::Matrix3 getMatrix3() - { - Ogre::Real a[3][3]; - for(size_t i = 0;i < 3;i++) - { - for(size_t j = 0;j < 3;j++) - a[i][j] = Ogre::Real(getFloat()); - } - return Ogre::Matrix3(a); - } - Ogre::Quaternion getQuaternion() - { - float a[4]; - for(size_t i = 0;i < 4;i++) - a[i] = getFloat(); - return Ogre::Quaternion(a); - } - Transformation getTrafo() - { - Transformation t; - t.pos = getVector3(); - t.rotation = getMatrix3(); - t.scale = getFloat(); - return t; - } - - std::string getString(size_t length) - { - std::vector str (length+1, 0); - - if(inp->read(&str[0], length) != length) - throw std::runtime_error ("string length in NIF file does not match"); - - return &str[0]; - } - std::string getString() - { - size_t size = read_le32(); - return getString(size); - } - - void getShorts(std::vector &vec, size_t size) - { - vec.resize(size); - for(size_t i = 0;i < vec.size();i++) - vec[i] = getShort(); - } - void getFloats(std::vector &vec, size_t size) - { - vec.resize(size); - for(size_t i = 0;i < vec.size();i++) - vec[i] = getFloat(); - } - void getVector2s(std::vector &vec, size_t size) - { - vec.resize(size); - for(size_t i = 0;i < vec.size();i++) - vec[i] = getVector2(); - } - void getVector3s(std::vector &vec, size_t size) - { - vec.resize(size); - for(size_t i = 0;i < vec.size();i++) - vec[i] = getVector3(); - } - void getVector4s(std::vector &vec, size_t size) - { - vec.resize(size); - for(size_t i = 0;i < vec.size();i++) - vec[i] = getVector4(); - } - void getQuaternions(std::vector &quat, size_t size) - { - quat.resize(size); - for(size_t i = 0;i < quat.size();i++) - quat[i] = getQuaternion(); - } + + Ogre::Vector2 getVector2(); + Ogre::Vector3 getVector3(); + Ogre::Vector4 getVector4(); + Ogre::Matrix3 getMatrix3(); + Ogre::Quaternion getQuaternion(); + Transformation getTrafo(); + + ///Read in a string of the given length + std::string getString(size_t length); + ///Read in a string of the length specified in the file + std::string getString(); + ///This is special since the version string doesn't start with a number, and ends with "\n" + std::string getVersionString(); + + void getShorts(std::vector &vec, size_t size); + void getFloats(std::vector &vec, size_t size); + void getVector2s(std::vector &vec, size_t size); + void getVector3s(std::vector &vec, size_t size); + void getVector4s(std::vector &vec, size_t size); + void getQuaternions(std::vector &quat, size_t size); }; } diff --git a/components/nif/node.cpp b/components/nif/node.cpp new file mode 100644 index 000000000..fb68da548 --- /dev/null +++ b/components/nif/node.cpp @@ -0,0 +1,60 @@ +#include "node.hpp" + +namespace Nif +{ + +void Node::getProperties(const Nif::NiTexturingProperty *&texprop, + const Nif::NiMaterialProperty *&matprop, + const Nif::NiAlphaProperty *&alphaprop, + const Nif::NiVertexColorProperty *&vertprop, + const Nif::NiZBufferProperty *&zprop, + const Nif::NiSpecularProperty *&specprop, + const Nif::NiWireframeProperty *&wireprop, + const Nif::NiStencilProperty *&stencilprop) const +{ + if(parent) + parent->getProperties(texprop, matprop, alphaprop, vertprop, zprop, specprop, wireprop, stencilprop); + + for(size_t i = 0;i < props.length();i++) + { + // Entries may be empty + if(props[i].empty()) + continue; + + const Nif::Property *pr = props[i].getPtr(); + if(pr->recType == Nif::RC_NiTexturingProperty) + texprop = static_cast(pr); + else if(pr->recType == Nif::RC_NiMaterialProperty) + matprop = static_cast(pr); + else if(pr->recType == Nif::RC_NiAlphaProperty) + alphaprop = static_cast(pr); + else if(pr->recType == Nif::RC_NiVertexColorProperty) + vertprop = static_cast(pr); + else if(pr->recType == Nif::RC_NiZBufferProperty) + zprop = static_cast(pr); + else if(pr->recType == Nif::RC_NiSpecularProperty) + specprop = static_cast(pr); + else if(pr->recType == Nif::RC_NiWireframeProperty) + wireprop = static_cast(pr); + else if (pr->recType == Nif::RC_NiStencilProperty) + stencilprop = static_cast(pr); + else + std::cerr<< "Unhandled property type: "<recName <getWorldTransform() * getLocalTransform(); + return getLocalTransform(); +} + +} diff --git a/components/nif/node.hpp b/components/nif/node.hpp index eebcd8be8..a26480d59 100644 --- a/components/nif/node.hpp +++ b/components/nif/node.hpp @@ -1,34 +1,15 @@ -/* - OpenMW - The completely unofficial reimplementation of Morrowind - Copyright (C) 2008-2010 Nicolay Korslund - Email: < korslund@gmail.com > - WWW: http://openmw.sourceforge.net/ - - This file (node.h) is part of the OpenMW package. - - OpenMW is distributed as free software: you can redistribute it - and/or modify it under the terms of the GNU General Public License - version 3, as published by the Free Software Foundation. - - This program is distributed in the hope that it will be useful, but - WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - General Public License for more details. - - You should have received a copy of the GNU General Public License - version 3 along with this program. If not, see - http://www.gnu.org/licenses/ . - - */ - #ifndef OPENMW_COMPONENTS_NIF_NODE_HPP #define OPENMW_COMPONENTS_NIF_NODE_HPP #include #include "controlled.hpp" +#include "extra.hpp" #include "data.hpp" #include "property.hpp" +#include "niftypes.hpp" +#include "controller.hpp" +#include "base.hpp" namespace Nif { @@ -117,7 +98,8 @@ public: const Nif::NiVertexColorProperty *&vertprop, const Nif::NiZBufferProperty *&zprop, const Nif::NiSpecularProperty *&specprop, - const Nif::NiWireframeProperty *&wireprop) const; + const Nif::NiWireframeProperty *&wireprop, + const Nif::NiStencilProperty *&stencilprop) const; Ogre::Matrix4 getLocalTransform() const; Ogre::Matrix4 getWorldTransform() const; @@ -149,6 +131,14 @@ struct NiNode : Node Node::read(nif); children.read(nif); effects.read(nif); + + // Discard tranformations for the root node, otherwise some meshes + // occasionally get wrong orientation. Only for NiNode-s for now, but + // can be expanded if needed. + if (0 == recIndex) + { + static_cast(this)->trafo = Nif::Transformation::getIdentity(); + } } void post(NIFFile *nif) diff --git a/components/nif/property.hpp b/components/nif/property.hpp index 06c8260ce..77f61d068 100644 --- a/components/nif/property.hpp +++ b/components/nif/property.hpp @@ -24,7 +24,7 @@ #ifndef OPENMW_COMPONENTS_NIF_PROPERTY_HPP #define OPENMW_COMPONENTS_NIF_PROPERTY_HPP -#include "controlled.hpp" +#include "base.hpp" namespace Nif { @@ -155,6 +155,22 @@ public: } }; +class NiFogProperty : public Property +{ +public: + float mFogDepth; + Ogre::Vector3 mColour; + + + void read(NIFStream *nif) + { + Property::read(nif); + + mFogDepth = nif->getFloat(); + mColour = nif->getVector3(); + } +}; + // These contain no other data than the 'flags' field in Property class NiShadeProperty : public Property { }; class NiDitherProperty : public Property { }; diff --git a/components/nif/record.hpp b/components/nif/record.hpp index 079b335f0..07d7540f8 100644 --- a/components/nif/record.hpp +++ b/components/nif/record.hpp @@ -44,6 +44,7 @@ enum RecordType RC_NiBSParticleNode, RC_NiCamera, RC_NiTexturingProperty, + RC_NiFogProperty, RC_NiMaterialProperty, RC_NiZBufferProperty, RC_NiAlphaProperty, diff --git a/components/nif/recordptr.hpp b/components/nif/recordptr.hpp index 1a4fc235b..5eac277d0 100644 --- a/components/nif/recordptr.hpp +++ b/components/nif/recordptr.hpp @@ -1,30 +1,8 @@ -/* - OpenMW - The completely unofficial reimplementation of Morrowind - Copyright (C) 2008-2010 Nicolay Korslund - Email: < korslund@gmail.com > - WWW: http://openmw.sourceforge.net/ - - This file (record_ptr.h) is part of the OpenMW package. - - OpenMW is distributed as free software: you can redistribute it - and/or modify it under the terms of the GNU General Public License - version 3, as published by the Free Software Foundation. - - This program is distributed in the hope that it will be useful, but - WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - General Public License for more details. - - You should have received a copy of the GNU General Public License - version 3 along with this program. If not, see - http://www.gnu.org/licenses/ . - - */ - #ifndef OPENMW_COMPONENTS_NIF_RECORDPTR_HPP #define OPENMW_COMPONENTS_NIF_RECORDPTR_HPP #include "niffile.hpp" +#include "nifstream.hpp" #include namespace Nif diff --git a/components/nif/tests/.gitignore b/components/nif/tests/.gitignore index b01c11f27..397b4a762 100644 --- a/components/nif/tests/.gitignore +++ b/components/nif/tests/.gitignore @@ -1,5 +1 @@ -niftool -*_test -*.nif -*.kf -output.txt +*.log diff --git a/components/nif/tests/CMakeLists.txt b/components/nif/tests/CMakeLists.txt new file mode 100644 index 000000000..a45298180 --- /dev/null +++ b/components/nif/tests/CMakeLists.txt @@ -0,0 +1,19 @@ +set(NIFTEST + niftest.cpp +) +source_group(components\\nif\\tests FILES ${NIFTEST}) + +# Main executable +add_executable(niftest + ${NIFTEST} +) + +target_link_libraries(niftest + ${Boost_LIBRARIES} + components +) + +if (BUILD_WITH_CODE_COVERAGE) + add_definitions (--coverage) + target_link_libraries(niftest gcov) +endif() diff --git a/components/nif/tests/Makefile b/components/nif/tests/Makefile deleted file mode 100644 index 0754bdfa6..000000000 --- a/components/nif/tests/Makefile +++ /dev/null @@ -1,12 +0,0 @@ -GCC=g++ - -all: niftool nif_bsa_test - -niftool: niftool.cpp ../nif_file.hpp ../nif_file.cpp ../record.hpp - $(GCC) $< ../nif_file.cpp ../../tools/stringops.cpp -o $@ - -nif_bsa_test: nif_bsa_test.cpp ../nif_file.cpp ../../bsa/bsa_file.cpp ../../tools/stringops.cpp - $(GCC) $^ -o $@ - -clean: - rm niftool *_test diff --git a/components/nif/tests/nif_bsa_test.cpp b/components/nif/tests/nif_bsa_test.cpp deleted file mode 100644 index c22aad680..000000000 --- a/components/nif/tests/nif_bsa_test.cpp +++ /dev/null @@ -1,30 +0,0 @@ -/* - Runs NIFFile through all the NIFs in Morrowind.bsa. - */ - -#include "../nif_file.hpp" -#include "../../bsa/bsa_file.hpp" -#include "../../tools/stringops.hpp" -#include - -using namespace Mangle::Stream; -using namespace std; -using namespace Nif; - -int main(int argc, char **args) -{ - BSAFile bsa; - cout << "Reading Morrowind.bsa\n"; - bsa.open("../../data/Morrowind.bsa"); - - const BSAFile::FileList &files = bsa.getList(); - - for(int i=0; i +#include +#include +#include +#include + +///See if the file has the named extension +bool hasExtension(std::string filename, std::string extensionToFind) +{ + std::string extension = filename.substr(filename.find_last_of(".")+1); + + //Convert strings to lower case for comparison + std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower); + std::transform(extensionToFind.begin(), extensionToFind.end(), extensionToFind.begin(), ::tolower); + + if(extension == extensionToFind) + return true; + else + return false; +} + +///See if the file has the "nif" extension. +bool isNIF(std::string filename) +{ + return hasExtension(filename,"nif"); +} +///See if the file has the "bsa" extension. +bool isBSA(std::string filename) +{ + return hasExtension(filename,"bsa"); +} + +///Check all the nif files in the given BSA archive +void readBSA(std::string filename) +{ + Bsa::BSAFile bsa; + bsa.open(filename.c_str()); + + const Bsa::BSAFile::FileList &files = bsa.getList(); + Bsa::addBSA(filename,"Bsa Files"); + + for(unsigned int i=0; i -#include -#include "../../mangle/stream/servers/file_stream.hpp" -#include "../node.hpp" -#include "../controller.hpp" -#include "../data.hpp" - -using namespace Mangle::Stream; -using namespace std; -using namespace Nif; - -// Display very verbose information -bool verbose = false; - -void doVector(const Vector *vec) -{ - cout << "[" - << vec->array[0] << "," - << vec->array[1] << "," - << vec->array[2] << "]\n"; -} - -void doVector4(const Vector4 *vec) -{ - cout << "[" - << vec->array[0] << "," - << vec->array[1] << "," - << vec->array[2] << "," - << vec->array[3] << "]\n"; -} - -void doMatrix(const Matrix *mat) -{ - cout << "Matrix:\n"; - for(int i=0; i<3; i++) - { - cout << " "; - doVector(&mat->v[i]); - } -} - -void doTrafo(const Transformation* trafo) -{ - cout << "--- transformation:\n"; - cout << "Pos: "; doVector(&trafo->pos); - cout << "Rot: "; doMatrix(&trafo->rotation); - cout << "Scale: " << trafo->scale << endl; - cout << "Vel: "; doVector(&trafo->velocity); - cout << "--- end transformation\n"; -} - -void doExtra(Extra *e) -{ - cout << "Extra: " << e->extra.getIndex() << endl; -} - -void doControlled(Controlled *c) -{ - doExtra(c); - cout << "Controller: " << c->controller.getIndex() << endl; -} - -void doNamed(Named *n) -{ - doControlled(n); - cout << "Name: " << n->name.toString() << endl; -} - -void doNode(Node *n) -{ - doNamed(n); - - cout << "Flags: 0x" << hex << n->flags << dec << endl; - doTrafo(n->trafo); - - cout << "Properties:"; - for(int i=0; iprops.length(); i++) - cout << " " << n->props.getIndex(i); - cout << endl; - - if(n->hasBounds) - { - cout << "Bounding box:\n"; - doVector(n->boundPos); - doMatrix(n->boundRot); - doVector(n->boundXYZ); - } - - if(n->boneTrafo) - { - cout << "This is a bone: "; - if(n->boneIndex == -1) - cout << "root bone\n"; - else - cout << "index " << n->boneIndex << endl; - } -} - -void doNiTriShape(NiTriShape *n) -{ - doNode(n); - - cout << "Shape data: " << n->data.getIndex() << endl; - cout << "Skin instance: " << n->skin.getIndex() << endl; -} - -void doNiSkinData(NiSkinData *n) -{ - int c = n->bones.size(); - - cout << "Global transformation:\n"; - doMatrix(&n->trafo->rotation); - doVector(&n->trafo->trans); - cout << "Scale: " << n->trafo->scale << endl; - - cout << "Bone number: " << c << endl; - for(int i=0; ibones[i]; - - cout << "-- Bone " << i << ":\n"; - doMatrix(&bi.trafo->rotation); - doVector(&bi.trafo->trans); - cout << "Scale: " << bi.trafo->scale << endl; - cout << "Unknown: "; doVector4(bi.unknown); - cout << "Weight number: " << bi.weights.length << endl; - - if(verbose) - for(int j=0; jdata.getIndex() << endl; - cout << "Root: " << n->root.getIndex() << endl; - cout << "Bones:"; - for(int i=0; ibones.length(); i++) - cout << " " << n->bones.getIndex(i); - cout << endl; -} - -void doNiNode(NiNode *n) -{ - doNode(n); - - cout << "Children:"; - for(int i=0; ichildren.length(); i++) - cout << " " << n->children.getIndex(i); - cout << endl; - - cout << "Effects:"; - for(int i=0; ieffects.length(); i++) - cout << " " << n->effects.getIndex(i); - cout << endl; -} - -void doNiStringExtraData(NiStringExtraData *s) -{ - doExtra(s); - cout << "String: " << s->string.toString() << endl; -} - -void doNiTextKeyExtraData(NiTextKeyExtraData *t) -{ - doExtra(t); - for(int i=0; ilist.size(); i++) - { - cout << "@time " << t->list[i].time << ":\n\"" - << t->list[i].text.toString() << "\"" << endl; - } -} - -void doController(Controller *r) -{ - cout << "Next controller: " << r->next.getIndex() << endl; - cout << "Flags: " << hex << r->flags << dec << endl; - cout << "Frequency: " << r->frequency << endl; - cout << "Phase: " << r->phase << endl; - cout << "Time start: " << r->timeStart << endl; - cout << "Time stop: " << r->timeStop << endl; - cout << "Target: " << r->target.getIndex() << endl; -} - -void doNiKeyframeController(NiKeyframeController *k) -{ - doController(k); - cout << "Data: " << k->data.getIndex() << endl; -} - -int main(int argc, char **args) -{ - if(argc != 2) - { - cout << "Specify a NIF file on the command line\n"; - return 1; - } - - StreamPtr file(new FileStream(args[1])); - NIFFile nif(file, args[1]); - - int num = nif.numRecords(); - - for(int i=0; irecName.toString() << endl; - - switch(r->recType) - { - case RC_NiNode: doNiNode((NiNode*)r); break; - case RC_NiSkinData: doNiSkinData((NiSkinData*)r); break; - case RC_NiSkinInstance: doNiSkinInstance((NiSkinInstance*)r); break; - case RC_NiTriShape: doNiTriShape((NiTriShape*)r); break; - case RC_NiStringExtraData: doNiStringExtraData((NiStringExtraData*)r); break; - case RC_NiSequenceStreamHelper: doNamed((Named*)r); break; - case RC_NiTextKeyExtraData: doNiTextKeyExtraData((NiTextKeyExtraData*)r); break; - case RC_NiKeyframeController: doNiKeyframeController((NiKeyframeController*)r); break; - } - - cout << endl; - } -} diff --git a/components/nif/tests/output/nif_bsa_test.out b/components/nif/tests/output/nif_bsa_test.out deleted file mode 100644 index 5499dafc7..000000000 --- a/components/nif/tests/output/nif_bsa_test.out +++ /dev/null @@ -1,5799 +0,0 @@ -Reading Morrowind.bsa -Decoding meshes\m\probe_journeyman_01.nif -Decoding meshes\b\b_n_redguard_f_skins.nif -Decoding meshes\b\b_n_redguard_m_skins.nif -Decoding meshes\b\b_n_redguard_f_wrist.nif -Decoding meshes\b\b_n_redguard_m_foot.nif -Decoding meshes\b\b_n_redguard_m_knee.nif -Decoding meshes\b\b_n_redguard_f_knee.nif -Decoding meshes\b\b_n_redguard_m_neck.nif -Decoding meshes\b\b_n_redguard_f_neck.nif -Decoding meshes\b\b_n_redguard_m_ankle.nif -Decoding meshes\b\b_n_redguard_f_ankle.nif -Decoding meshes\b\b_n_redguard_f_foot.nif -Decoding meshes\b\b_n_redguard_m_wrist.nif -Decoding meshes\b\b_n_redguard_f_groin.nif -Decoding meshes\b\b_n_redguard_m_groin.nif -Decoding meshes\b\b_n_redguard_m_head_02.nif -Decoding meshes\b\b_n_redguard_m_head_06.nif -Decoding meshes\b\b_n_redguard_m_head_04.nif -Decoding meshes\b\b_n_redguard_f_head_02.nif -Decoding meshes\b\b_n_redguard_f_head_06.nif -Decoding meshes\b\b_n_redguard_f_head_04.nif -Decoding meshes\b\b_n_redguard_m_hair_05.nif -Decoding meshes\b\b_n_redguard_m_hair_03.nif -Decoding meshes\b\b_n_redguard_m_hair_01.nif -Decoding meshes\b\b_n_redguard_f_hair_05.nif -Decoding meshes\b\b_n_redguard_f_hair_03.nif -Decoding meshes\b\b_n_redguard_f_hair_01.nif -Decoding meshes\b\b_n_redguard_m_forearm.nif -Decoding meshes\b\b_n_redguard_m_head_03.nif -Decoding meshes\b\b_n_redguard_m_head_01.nif -Decoding meshes\b\b_n_redguard_m_head_05.nif -Decoding meshes\b\b_n_redguard_f_head_03.nif -Decoding meshes\b\b_n_redguard_f_head_01.nif -Decoding meshes\b\b_n_redguard_f_head_05.nif -Decoding meshes\b\b_n_redguard_m_hair_06.nif -Decoding meshes\b\b_n_redguard_m_hair_04.nif -Decoding meshes\b\b_n_redguard_m_hair_02.nif -Decoding meshes\b\b_n_redguard_m_hair_00.nif -Decoding meshes\b\b_n_redguard_f_hair_04.nif -Decoding meshes\b\b_n_redguard_f_hair_02.nif -Decoding meshes\b\b_n_redguard_f_forearm.nif -Decoding meshes\b\b_n_redguard_f_upper leg.nif -Decoding meshes\b\b_n_redguard_f_upper arm.nif -Decoding meshes\b\b_n_redguard_m_upper leg.nif -Decoding meshes\b\b_n_redguard_m_hands.1st.nif -Decoding meshes\b\b_n_redguard_m_upper arm.nif -Decoding meshes\b\b_n_redguard_f_hands.1st.nif -Decoding meshes\w\w_longspear_daedric.nif -Decoding meshes\w\w_longsword_crystal.nif -Decoding meshes\w\w_longsword_daedric.nif -Decoding meshes\l\light_com_lantern_01.nif -Decoding meshes\l\light_com_candle_15.nif -Decoding meshes\l\light_com_candle_05.nif -Decoding meshes\l\light_com_candle_14.nif -Decoding meshes\l\light_com_candle_04.nif -Decoding meshes\l\light_com_candle_07.nif -Decoding meshes\l\light_com_candle_16.nif -Decoding meshes\l\light_com_candle_06.nif -Decoding meshes\l\light_com_candle_11.nif -Decoding meshes\l\light_com_candle_01.nif -Decoding meshes\l\light_com_candle_10.nif -Decoding meshes\l\light_com_candle_13.nif -Decoding meshes\l\light_com_candle_03.nif -Decoding meshes\l\light_com_candle_12.nif -Decoding meshes\l\light_com_candle_02.nif -Decoding meshes\l\light_com_candle_09.nif -Decoding meshes\l\light_com_candle_08.nif -Decoding meshes\l\light_com_sconce_02.nif -Decoding meshes\l\light_com_sconce_01.nif -Decoding meshes\l\light_com_lantern_02.nif -Decoding meshes\l\light_com_chandelier_03.nif -Decoding meshes\l\light_com_chandelier_01.nif -Decoding meshes\l\light_com_chandelier_05.nif -Decoding meshes\l\light_com_chandelier_02.nif -Decoding meshes\l\light_com_chandelier_06.nif -Decoding meshes\l\light_com_chandelier_04.nif -Decoding meshes\f\flora_ash_grass_b_01.nif -Decoding meshes\f\flora_ash_grass_r_01.nif -Decoding meshes\f\flora_ash_grass_w_01.nif -Decoding meshes\a\a_art_helm_bearclaw.nif -Decoding meshes\l\light_com_chandelier_01_l.nif -Decoding meshes\l\light_com_chandelier_05_l.nif -Decoding meshes\l\light_com_chandelier_03_l.nif -Decoding meshes\b\b_n_breton_f_head_04.nif -Decoding meshes\b\b_n_breton_f_hair_04.nif -Decoding meshes\b\b_n_breton_m_head_01.nif -Decoding meshes\b\b_n_breton_f_head_01.nif -Decoding meshes\b\b_n_breton_m_head_03.nif -Decoding meshes\b\b_n_breton_m_hair_01.nif -Decoding meshes\b\b_n_breton_m_head_04.nif -Decoding meshes\b\b_n_breton_f_head_05.nif -Decoding meshes\b\b_n_breton_m_head_07.nif -Decoding meshes\b\b_n_breton_m_head_08.nif -Decoding meshes\b\b_n_breton_m_head_06.nif -Decoding meshes\b\b_n_breton_m_forearm.nif -Decoding meshes\b\b_n_breton_m_head_05.nif -Decoding meshes\b\b_n_breton_f_head_06.nif -Decoding meshes\b\b_n_breton_f_hair_03.nif -Decoding meshes\b\b_n_breton_m_hair_04.nif -Decoding meshes\b\b_n_breton_m_hair_05.nif -Decoding meshes\b\b_n_breton_m_hair_03.nif -Decoding meshes\b\b_n_breton_f_head_03.nif -Decoding meshes\b\b_n_breton_f_hair_02.nif -Decoding meshes\b\b_n_breton_m_head_02.nif -Decoding meshes\b\b_n_breton_f_forearm.nif -Decoding meshes\b\b_n_breton_m_hair_02.nif -Decoding meshes\b\b_n_breton_f_head_02.nif -Decoding meshes\b\b_n_breton_m_hair_00.nif -Decoding meshes\b\b_n_breton_f_hair_05.nif -Decoding meshes\b\b_n_breton_f_hair_01.nif -Decoding meshes\b\b_n_breton_m_upper leg.nif -Decoding meshes\b\b_n_breton_f_upper leg.nif -Decoding meshes\b\b_n_breton_f_upper arm.nif -Decoding meshes\b\b_n_breton_m_upper arm.nif -Decoding meshes\b\b_n_breton_m_hand.1st.nif -Decoding meshes\b\b_n_breton_f_hands.1st.nif -Decoding meshes\a\a_gondolier_m_helmet.nif -Decoding meshes\m\light_com_candle_07.nif -Decoding meshes\m\misc_dwrv_ark_cube00.nif -Decoding meshes\m\misc_dwrv_artifact30.nif -Decoding meshes\m\misc_dwrv_artifact20.nif -Decoding meshes\m\misc_dwrv_artifact10.nif -Decoding meshes\m\misc_dwrv_artifact00.nif -Decoding meshes\m\misc_dwrv_artifact70.nif -Decoding meshes\m\misc_dwrv_artifact60.nif -Decoding meshes\m\misc_dwrv_artifact50.nif -Decoding meshes\m\misc_dwrv_artifact40.nif -Decoding meshes\m\misc_dwrv_artifact80.nif -Decoding meshes\m\misc_dwrv_pitcher00.nif -Decoding meshes\m\misc_dwrv_ark_key00.nif -Decoding meshes\i\in_dae_hall_l_corner.nif -Decoding meshes\i\in_dae_doorjamb_load.nif -Decoding meshes\i\in_dae_connect_lcave.nif -Decoding meshes\i\in_dae_platform_stairs.nif -Decoding meshes\i\in_dae_pillar_tall.max.nif -Decoding meshes\i\in_dae_platform_512_01.nif -Decoding meshes\i\in_dae_room_l_roof_01.nif -Decoding meshes\i\in_dae_room_l_floor_01.nif -Decoding meshes\i\in_dae_room_l_roof_02.nif -Decoding meshes\i\in_dae_room_l_side_01.nif -Decoding meshes\i\in_dae_room_l_roof_03.nif -Decoding meshes\i\in_dae_hall_ruin_l_02.nif -Decoding meshes\i\in_dae_hall_l_entry_01.nif -Decoding meshes\i\in_dae_hall_l_4way_02.nif -Decoding meshes\i\in_dae_hall_ruin_l_01.nif -Decoding meshes\i\in_dae_hall_l_3way_01.nif -Decoding meshes\i\in_dae_hall_l_4way_01.nif -Decoding meshes\i\in_dae_mezzanine_edge.nif -Decoding meshes\i\in_dae_room_ruin_roof_03.nif -Decoding meshes\i\in_dae_room_r_corner_02.nif -Decoding meshes\i\in_dae_room_l_corner_02.nif -Decoding meshes\i\in_dae_room_r_corner_01.nif -Decoding meshes\i\in_dae_room_l_corner_01.nif -Decoding meshes\i\in_dae_room_ruin_side_01.nif -Decoding meshes\i\in_dae_hall_l_endcap_01.nif -Decoding meshes\i\in_dae_hall_l_stairs_01.nif -Decoding meshes\i\in_com_trapbottom_01.nif -Decoding meshes\i\in_t_l_room_ceiling.nif -Decoding meshes\i\in_v_l_int_center_02.nif -Decoding meshes\i\in_v_l_int_corner_01.nif -Decoding meshes\i\in_v_l_int_stairs_04.nif -Decoding meshes\i\in_v_l_int_column_03.nif -Decoding meshes\i\in_v_l_int_column_01.nif -Decoding meshes\i\in_v_l_int_pillar_01.nif -Decoding meshes\i\in_v_l_int_bridge_02.nif -Decoding meshes\i\in_v_l_int_stairs_02.nif -Decoding meshes\i\in_v_l_int_stairs_03.nif -Decoding meshes\i\in_v_l_int_stairs_01.nif -Decoding meshes\i\in_v_l_int_lwall_01.nif -Decoding meshes\i\in_v_l_int_lwall_02.nif -Decoding meshes\i\in_v_l_int_center_01.nif -Decoding meshes\i\in_v_l_int_corner_03.nif -Decoding meshes\i\in_v_l_int_column_02.nif -Decoding meshes\i\in_v_l_int_bridge_01.nif -Decoding meshes\i\in_v_l_int_corner_02.nif -Decoding meshes\i\in_v_l_int_arches_01.nif -Decoding meshes\i\in_r_l_int_center_02.nif -Decoding meshes\i\in_r_l_int_corner_01.nif -Decoding meshes\i\in_r_l_int_column_01.nif -Decoding meshes\i\in_r_l_int_pillar_01.nif -Decoding meshes\i\in_r_l_int_bridge_02.nif -Decoding meshes\i\in_r_l_int_stairs_02.nif -Decoding meshes\i\in_r_l_short_ramp_01.nif -Decoding meshes\i\in_r_l_int_stairs_03.nif -Decoding meshes\i\in_r_l_int_stairs_01.nif -Decoding meshes\i\in_r_l_long_ramp_01.nif -Decoding meshes\i\in_r_l_int_lwall_01.nif -Decoding meshes\i\in_r_l_int_lwall_02.nif -Decoding meshes\i\in_r_l_int_center_01.nif -Decoding meshes\i\in_r_l_int_corner_03.nif -Decoding meshes\i\in_r_l_int_pillar_02.nif -Decoding meshes\i\in_r_l_int_bridge_01.nif -Decoding meshes\i\in_r_l_int_corner_02.nif -Decoding meshes\i\in_r_l_int_arches_01.nif -Decoding meshes\i\in_v_l_int_lcenter_02.nif -Decoding meshes\i\in_v_l_int_lcorner_01.nif -Decoding meshes\i\in_v_l_int_lcolumn_03.nif -Decoding meshes\i\in_v_l_int_lcolumn_01.nif -Decoding meshes\i\in_v_l_int_ceiling_01.nif -Decoding meshes\i\in_v_l_int_entrance_02.nif -Decoding meshes\i\in_v_l_int_lcorner_03.nif -Decoding meshes\i\in_v_l_int_lcenter_01.nif -Decoding meshes\i\in_v_l_int_lcolumn_02.nif -Decoding meshes\i\in_v_l_int_lcorner_02.nif -Decoding meshes\i\in_v_l_int_entrance_01.nif -Decoding meshes\i\in_r_l_int_lcenter_02.nif -Decoding meshes\i\in_r_l_int_lcorner_01.nif -Decoding meshes\i\in_r_l_int_lcolumn_01.nif -Decoding meshes\i\in_r_l_int_entrance_02.nif -Decoding meshes\i\in_r_l_int_lcorner_03.nif -Decoding meshes\i\in_r_l_int_lcenter_01.nif -Decoding meshes\i\in_r_l_int_lcenter_04.nif -Decoding meshes\i\in_r_l_int_lcenter_03.nif -Decoding meshes\i\in_r_l_int_balcony_01.nif -Decoding meshes\i\in_r_l_int_railing_01.nif -Decoding meshes\i\in_r_l_int_lcorner_02.nif -Decoding meshes\i\in_r_l_int_entrance_01.nif -Decoding meshes\i\in_r_l_int_entrance_03.nif -Decoding meshes\i\in_t_l_room_highentry.nif -Decoding meshes\m\misc_kwamaegg_gold_01.nif -Decoding meshes\i\in_v_l_int_lentrance_02.nif -Decoding meshes\i\in_v_l_int_partition_01.nif -Decoding meshes\i\in_v_l_int_lentrance_01.nif -Decoding meshes\i\in_r_l_int_lentrance_02.nif -Decoding meshes\i\in_r_l_int_partition_01.nif -Decoding meshes\i\in_r_l_int_lentrance_01.nif -Decoding meshes\i\in_r_l_int_lpartition_01.nif -Decoding meshes\i\cap01.nif -Decoding meshes\i\box02.nif -Decoding meshes\i\in_ar_s2.nif -Decoding meshes\i\in_ar_s3.nif -Decoding meshes\i\in_ar_s1.nif -Decoding meshes\i\in_ar_s6.nif -Decoding meshes\i\in_ar_s7.nif -Decoding meshes\i\in_ar_s4.nif -Decoding meshes\i\in_ar_s5.nif -Decoding meshes\i\in_ar_02.nif -Decoding meshes\i\in_ar_03.nif -Decoding meshes\i\in_ar_01.nif -Decoding meshes\i\in_ar_06.nif -Decoding meshes\i\in_ar_07.nif -Decoding meshes\i\in_ar_04.nif -Decoding meshes\i\in_ar_05.nif -Decoding meshes\i\in_ar_08.nif -Decoding meshes\i\in_ar_09.nif -Decoding meshes\i\in_ci_01.nif -Decoding meshes\i\in_ar_10.nif -Decoding meshes\i\in_imp_fireplace_grand.nif -Decoding meshes\i\in_t_s_plain_hall_01.nif -Decoding meshes\i\in_t_s_hallshaft_cap.nif -Decoding meshes\i\in_t_s_hall_ramp_01.nif -Decoding meshes\i\in_t_s_room_side_01.nif -Decoding meshes\i\in_v_s_int_center_02.nif -Decoding meshes\i\in_v_s_int_corner_01.nif -Decoding meshes\i\in_v_s_int_column_01.nif -Decoding meshes\i\in_v_s_int_lwall_01.nif -Decoding meshes\i\in_v_s_int_lwall_03.nif -Decoding meshes\i\in_v_s_int_lwall_02.nif -Decoding meshes\i\in_v_s_int_lwall_04.nif -Decoding meshes\i\in_v_s_int_center_01.nif -Decoding meshes\i\in_v_s_int_corner_03.nif -Decoding meshes\i\in_v_s_int_center_04.nif -Decoding meshes\i\in_v_s_int_center_03.nif -Decoding meshes\i\in_v_s_int_corner_02.nif -Decoding meshes\i\in_v_s_int_lrail_01.nif -Decoding meshes\i\in_r_s_int_center_02.nif -Decoding meshes\i\in_r_s_int_center_06.nif -Decoding meshes\i\in_r_s_int_corner_01.nif -Decoding meshes\i\in_r_s_int_stairs_04.nif -Decoding meshes\i\in_r_s_int_column_01.nif -Decoding meshes\i\in_r_s_int_pillar_01.nif -Decoding meshes\i\in_r_s_int_bridge_02.nif -Decoding meshes\i\in_r_s_int_ledge_03.nif -Decoding meshes\i\in_r_s_int_ledge_02.nif -Decoding meshes\i\in_r_s_int_ledge_01.nif -Decoding meshes\i\in_r_s_int_bridge_03.nif -Decoding meshes\i\in_r_s_int_stairs_05.nif -Decoding meshes\i\in_r_s_int_stairs_02.nif -Decoding meshes\i\in_r_s_int_center_05.nif -Decoding meshes\i\in_r_s_short_ramp_01.nif -Decoding meshes\i\in_r_s_int_stairs_03.nif -Decoding meshes\i\in_r_s_int_stairs_01.nif -Decoding meshes\i\in_r_s_long_ramp_01.nif -Decoding meshes\i\in_r_s_int_lwall_01.nif -Decoding meshes\i\in_r_s_int_lwall_02.nif -Decoding meshes\i\in_r_s_int_center_01.nif -Decoding meshes\i\in_r_s_int_corner_03.nif -Decoding meshes\i\in_r_s_int_center_04.nif -Decoding meshes\i\in_r_s_int_pillar_02.nif -Decoding meshes\i\in_r_s_int_center_03.nif -Decoding meshes\i\in_r_s_int_bridge_01.nif -Decoding meshes\i\in_r_s_int_corner_02.nif -Decoding meshes\i\in_r_s_int_steps_01.nif -Decoding meshes\i\in_r_s_int_steps_02.nif -Decoding meshes\i\in_r_s_int_steps_03.nif -Decoding meshes\i\in_r_s_int_stairs_06.nif -Decoding meshes\i\in_t_s_hall_endcap_01.nif -Decoding meshes\i\in_v_s_int_lcenter_02.nif -Decoding meshes\i\in_v_s_int_lstairs_04.nif -Decoding meshes\i\in_v_s_int_lcorner_01.nif -Decoding meshes\i\in_v_s_int_lcolumn_03.nif -Decoding meshes\i\in_v_s_int_lcolumn_01.nif -Decoding meshes\i\in_v_s_int_lbridge_02.nif -Decoding meshes\i\in_v_s_int_lpillar_01.nif -Decoding meshes\i\in_v_s_int_entrance_02.nif -Decoding meshes\i\in_v_s_int_lstairs_03.nif -Decoding meshes\i\in_v_s_int_lstairs_02.nif -Decoding meshes\i\in_v_s_int_lcorner_03.nif -Decoding meshes\i\in_v_s_int_lcenter_01.nif -Decoding meshes\i\in_v_s_int_lcenter_04.nif -Decoding meshes\i\in_v_s_int_lstairs_01.nif -Decoding meshes\i\in_v_s_int_lcolumn_02.nif -Decoding meshes\i\in_v_s_int_larches_01.nif -Decoding meshes\i\in_v_s_int_lcenter_03.nif -Decoding meshes\i\in_v_s_int_lcorner_02.nif -Decoding meshes\i\in_v_s_int_lbridge_01.nif -Decoding meshes\i\in_v_s_int_entrance_01.nif -Decoding meshes\i\in_v_s_lint_center_01.nif -Decoding meshes\i\in_r_s_int_lcenter_02.nif -Decoding meshes\i\in_r_s_int_lcenter_06.nif -Decoding meshes\i\in_r_s_int_lcorner_01.nif -Decoding meshes\i\in_r_s_int_lcolumn_01.nif -Decoding meshes\i\in_r_s_int_entrance_02.nif -Decoding meshes\i\in_r_s_int_lcenter_05.nif -Decoding meshes\i\in_r_s_int_lcorner_03.nif -Decoding meshes\i\in_r_s_int_lcenter_01.nif -Decoding meshes\i\in_r_s_int_lcenter_04.nif -Decoding meshes\i\in_r_s_int_larches_01.nif -Decoding meshes\i\in_r_s_int_lcenter_03.nif -Decoding meshes\i\in_r_s_int_balcony_01.nif -Decoding meshes\i\in_r_s_int_railing_01.nif -Decoding meshes\i\in_r_s_int_lcorner_02.nif -Decoding meshes\i\in_r_s_int_entrance_01.nif -Decoding meshes\i\in_t_s_pillar_large_01.nif -Decoding meshes\i\in_t_s_plain_hall_ramp.nif -Decoding meshes\i\in_t_s_plain_turret_02.nif -Decoding meshes\i\in_t_s_plain_shaft_cap.nif -Decoding meshes\i\in_t_s_plain_hall_4way.nif -Decoding meshes\i\in_t_s_plain_hall_3way.nif -Decoding meshes\i\in_t_s_plain_hall_plug.nif -Decoding meshes\i\in_t_s_shaft_elbow_01.nif -Decoding meshes\i\in_t_s_shaft_vconnect.nif -Decoding meshes\i\in_v_s_wall_column_01.nif -Decoding meshes\i\in_v_s_wall_column_02.nif -Decoding meshes\i\in_t_s_hall_small_corner.nif -Decoding meshes\i\in_t_s_plain_hall_corner.nif -Decoding meshes\i\in_t_s_plain_hall_endcap.nif -Decoding meshes\i\in_t_s_plain_room_center.nif -Decoding meshes\i\in_v_s_int_lentrance_02.nif -Decoding meshes\i\in_v_s_int_lentrance_04.nif -Decoding meshes\i\in_v_s_int_lentrance_06.nif -Decoding meshes\i\in_v_s_int_lentrance_01.nif -Decoding meshes\i\in_v_s_int_lentrance_03.nif -Decoding meshes\i\in_v_s_int_lentrance_05.nif -Decoding meshes\i\in_v_s_int_lpartition_01.nif -Decoding meshes\i\in_r_s_int_lentrance_02.nif -Decoding meshes\i\in_r_s_int_lentrance_04.nif -Decoding meshes\i\in_r_s_int_lentrance_06.nif -Decoding meshes\i\in_r_s_int_partition_01.nif -Decoding meshes\i\in_r_s_int_lentrance_01.nif -Decoding meshes\i\in_r_s_int_lentrance_03.nif -Decoding meshes\i\in_r_s_int_lentrance_05.nif -Decoding meshes\i\in_r_s_int_lplatform_01.nif -Decoding meshes\i\in_r_s_int_lpartition_01.nif -Decoding meshes\m\apparatus_a_spipe_01.nif -Decoding meshes\m\apparatus_s_alembic_01.nif -Decoding meshes\m\apparatus_g_alembic_01.nif -Decoding meshes\m\apparatus_a_alembic_01.nif -Decoding meshes\m\apparatus_m_alembic_01.nif -Decoding meshes\m\apparatus_j_alembic_01.nif -Decoding meshes\m\apparatus_s_retort_01.nif -Decoding meshes\m\apparatus_m_retort_01.nif -Decoding meshes\m\apparatus_j_retort_01.nif -Decoding meshes\m\apparatus_g_retort_01.nif -Decoding meshes\m\apparatus_a_retort_01.nif -Decoding meshes\i\in_dae_pillar_verytall.max.nif -Decoding meshes\i\in_dae_hall_l_staircurve2.nif -Decoding meshes\i\in_dae_hall_l_staircurve1.nif -Decoding meshes\i\in_dae_room_l_cornerout_01.nif -Decoding meshes\i\in_dae_room_ruin_cornerout.nif -Decoding meshes\i\scene root.nif -Decoding meshes\i\in_t_s_plain_hallshaft_cap.nif -Decoding meshes\m\apparatus_a_calcinator_01.nif -Decoding meshes\a\a_dragonscale_cuirass.nif -Decoding meshes\a\a_dragonscale_cuir_gnd.nif -Decoding meshes\l\light_ashl_lantern_01.nif -Decoding meshes\l\light_ashl_lantern_04.nif -Decoding meshes\l\light_ashl_lantern_05.nif -Decoding meshes\l\light_ashl_lantern_07.nif -Decoding meshes\l\light_ashl_lantern_03.nif -Decoding meshes\l\light_ashl_lantern_02.nif -Decoding meshes\l\light_ashl_lantern_06.nif -Decoding meshes\d\door_cavern_doors20.nif -Decoding meshes\d\door_cavern_doors10.nif -Decoding meshes\d\door_cavern_doors00.nif -Decoding meshes\m\misc_6th_ash_statue_01.nif -Decoding meshes\b\b_n_argonian_m_wrist.nif -Decoding meshes\b\b_n_argonian_m_skins.nif -Decoding meshes\b\b_n_argonian_f_skins.nif -Decoding meshes\b\b_n_argonian_f_neck.nif -Decoding meshes\b\b_n_argonian_m_neck.nif -Decoding meshes\b\b_n_argonian_f_wrist.nif -Decoding meshes\b\b_n_argonian_f_knee.nif -Decoding meshes\b\b_n_argonian_m_groin.nif -Decoding meshes\b\b_n_argonian_m_knee.nif -Decoding meshes\b\b_n_argonian_f_groin.nif -Decoding meshes\b\b_n_argonian_f_ankle.nif -Decoding meshes\b\b_n_argonian_m_ankle.nif -Decoding meshes\b\b_n_argonian_m_hair02.nif -Decoding meshes\b\b_n_argonian_f_hair02.nif -Decoding meshes\b\b_n_argonian_m_hair06.nif -Decoding meshes\b\b_n_argonian_m_head_02.nif -Decoding meshes\b\b_n_argonian_f_head_02.nif -Decoding meshes\b\b_n_argonian_f_hair03.nif -Decoding meshes\b\b_n_argonian_m_hair03.nif -Decoding meshes\b\b_n_argonian_m_forearm.nif -Decoding meshes\b\b_n_argonian_m_hair04.nif -Decoding meshes\b\b_n_argonian_f_hair04.nif -Decoding meshes\b\b_n_argonian_f_hair01.nif -Decoding meshes\b\b_n_argonian_m_hair01.nif -Decoding meshes\b\b_n_argonian_m_head_03.nif -Decoding meshes\b\b_n_argonian_m_head_01.nif -Decoding meshes\b\b_n_argonian_f_head_03.nif -Decoding meshes\b\b_n_argonian_f_head_01.nif -Decoding meshes\b\b_n_argonian_m_hair05.nif -Decoding meshes\b\b_n_argonian_f_hair05.nif -Decoding meshes\b\b_n_argonian_f_upper leg.nif -Decoding meshes\b\b_n_argonian_m_upper leg.nif -Decoding meshes\b\b_n_argonian_m_hands.1st.nif -Decoding meshes\b\b_n_argonian_f_hands.1st.nif -Decoding meshes\b\b_n_argonian_f_upper arm.nif -Decoding meshes\b\b_n_argonian_m_upper arm.nif -Decoding meshes\c\c_f_pants_g_common00.nif -Decoding meshes\c\c_f_pants_a_common00.nif -Decoding meshes\c\c_f_pants_k_common00.nif -Decoding meshes\c\c_f_pants_k_common02.nif -Decoding meshes\c\c_f_pants_a_common02.nif -Decoding meshes\c\c_f_pants_g_common02.nif -Decoding meshes\c\c_f_pants_k_common01.nif -Decoding meshes\c\c_f_pants_g_common01.nif -Decoding meshes\c\c_f_pants_a_common01.nif -Decoding meshes\c\c_f_pants_ul_common00.nif -Decoding meshes\c\c_f_pants_ul_common02.nif -Decoding meshes\c\c_f_pants_ul_common01.nif -Decoding meshes\c\c_f_pants_common_4_b_a.nif -Decoding meshes\c\c_f_pants_common_4_b_g.nif -Decoding meshes\c\c_f_pants_common_4_b_k.nif -Decoding meshes\c\c_f_pants_common_4_b_ul.nif -Decoding meshes\b\b_n_high elf_f_skins.nif -Decoding meshes\b\b_n_high elf_m_skins.nif -Decoding meshes\b\b_n_high elf_m_foot.nif -Decoding meshes\b\b_n_high elf_m_wrist.nif -Decoding meshes\b\b_n_high elf_m_knee.nif -Decoding meshes\b\b_n_high elf_f_knee.nif -Decoding meshes\b\b_n_high elf_f_groin.nif -Decoding meshes\b\b_n_high elf_m_groin.nif -Decoding meshes\b\b_n_high elf_m_neck.nif -Decoding meshes\b\b_n_high elf_f_neck.nif -Decoding meshes\b\b_n_high elf_f_wrist.nif -Decoding meshes\b\b_n_high elf_f_foot.nif -Decoding meshes\b\b_n_high elf_m_ankle.nif -Decoding meshes\b\b_n_high elf_f_ankle.nif -Decoding meshes\b\b_n_high elf_f_head_02.nif -Decoding meshes\b\b_n_high elf_f_head_06.nif -Decoding meshes\b\b_n_high elf_f_head_04.nif -Decoding meshes\b\b_n_high elf_m_head_02.nif -Decoding meshes\b\b_n_high elf_m_head_06.nif -Decoding meshes\b\b_n_high elf_m_head_04.nif -Decoding meshes\b\b_n_high elf_f_hair_01.nif -Decoding meshes\b\b_n_high elf_f_hair_03.nif -Decoding meshes\b\b_n_high elf_m_hair_05.nif -Decoding meshes\b\b_n_high elf_m_hair_01.nif -Decoding meshes\b\b_n_high elf_m_hair_03.nif -Decoding meshes\b\b_n_high elf_m_forearm.nif -Decoding meshes\b\b_n_high elf_f_head_03.nif -Decoding meshes\b\b_n_high elf_f_head_01.nif -Decoding meshes\b\b_n_high elf_f_head_05.nif -Decoding meshes\b\b_n_high elf_m_head_03.nif -Decoding meshes\b\b_n_high elf_m_head_01.nif -Decoding meshes\b\b_n_high elf_m_head_05.nif -Decoding meshes\b\b_n_high elf_f_hair_04.nif -Decoding meshes\b\b_n_high elf_f_hair_02.nif -Decoding meshes\b\b_n_high elf_m_hair_04.nif -Decoding meshes\b\b_n_high elf_m_hair_02.nif -Decoding meshes\b\b_n_high elf_f_forearm.nif -Decoding meshes\b\b_n_high elf_m_upper arm.nif -Decoding meshes\b\b_n_high elf_f_upper leg.nif -Decoding meshes\b\b_n_high elf_m_upper leg.nif -Decoding meshes\b\b_n_high elf_f_hands.1st.nif -Decoding meshes\b\b_n_high elf_m_hands.1st.nif -Decoding meshes\b\b_n_high elf_f_upper arm.nif -Decoding meshes\f\flora_bc_mushroom_08.nif -Decoding meshes\f\flora_bc_lilypad_02.nif -Decoding meshes\f\flora_bc_lilypad_03.nif -Decoding meshes\f\flora_bc_lilypad_01.nif -Decoding meshes\f\flora_bc_mushroom_05.nif -Decoding meshes\f\flora_bc_mushroom_01.nif -Decoding meshes\f\flora_bc_mushroom_02.nif -Decoding meshes\f\flora_bc_mushroom_03.nif -Decoding meshes\f\flora_bc_podplant_03.nif -Decoding meshes\f\flora_bc_podplant_02.nif -Decoding meshes\f\flora_bc_mushroom_07.nif -Decoding meshes\f\flora_bc_mushroom_06.nif -Decoding meshes\f\flora_bc_podplant_01.nif -Decoding meshes\f\flora_bc_mushroom_04.nif -Decoding meshes\f\flora_bc_shelffungus_02.nif -Decoding meshes\f\flora_bc_shelffungus_04.nif -Decoding meshes\f\flora_bc_shelffungus_03.nif -Decoding meshes\f\flora_bc_shelffungus_01.nif -Decoding meshes\m\misc_muck_shovel_01.nif -Decoding meshes\m\pick_secretmaster_01.nif -Decoding meshes\c\c_f_shirt_c_common01.nif -Decoding meshes\c\c_f_shirt_common_4_a_c.nif -Decoding meshes\c\c_f_shirt_common_4_c_c.nif -Decoding meshes\c\c_f_shirt_common_4_b_c.nif -Decoding meshes\c\c_f_shirt_c_commonl04.nif -Decoding meshes\c\c_f_shirt_c_commonl02.nif -Decoding meshes\c\c_f_skirt_g_common01.nif -Decoding meshes\c\c_f_skirt_g_common_4_c.nif -Decoding meshes\f\flora_emp_parasol_01.nif -Decoding meshes\f\flora_emp_parasol_02.nif -Decoding meshes\f\flora_emp_parasol_03.nif -Decoding meshes\c\c_m_robe_common_02r.nif -Decoding meshes\c\c_m_robe_common_02t.nif -Decoding meshes\c\c_m_robe_common_03a.nif -Decoding meshes\c\c_m_robe_common_05a.nif -Decoding meshes\c\c_m_robe_common_05c.nif -Decoding meshes\c\c_m_robe_common_03b.nif -Decoding meshes\c\c_m_robe_common_05b.nif -Decoding meshes\c\c_m_robe_common_02h.nif -Decoding meshes\c\c_m_robe_common_02tt.nif -Decoding meshes\c\c_m_robe_expensive_1.nif -Decoding meshes\c\c_m_robe_exquisite_1.nif -Decoding meshes\c\c_m_robe_common_02rr.nif -Decoding meshes\c\c_m_robe_extrav_1_c.nif -Decoding meshes\c\c_m_robe_common_02hh.nif -Decoding meshes\c\c_m_robe_expensive_2.nif -Decoding meshes\c\c_m_robe_common_02_gnd.nif -Decoding meshes\c\c_m_robe_common_01_gnd.nif -Decoding meshes\c\c_m_robe_extrav_2_gnd.nif -Decoding meshes\c\c_m_robe_expens_3.1st.nif -Decoding meshes\c\c_m_robe_common_4.1st.nif -Decoding meshes\c\c_m_robe_common_4_gnd.nif -Decoding meshes\c\c_m_robe_common_3_gnd.nif -Decoding meshes\c\c_m_robe_common_5.1st.nif -Decoding meshes\c\c_m_robe_common_02.1st.nif -Decoding meshes\c\c_m_robe_common_01.1st.nif -Decoding meshes\c\c_m_robe_expens_3_gnd.nif -Decoding meshes\c\c_m_robe_common_3.1st.nif -Decoding meshes\c\c_m_robe_extrav_1_gnd.nif -Decoding meshes\c\c_m_robe_extrav_2.1st.nif -Decoding meshes\c\c_m_robe_extrav_1r_gnd.nif -Decoding meshes\c\c_m_robe_extrav_1t_gnd.nif -Decoding meshes\c\c_m_robe_extrav_1h_gnd.nif -Decoding meshes\c\c_m_robe_extrav_1a_gnd.nif -Decoding meshes\c\c_m_robe_extrav_1b_gnd.nif -Decoding meshes\c\c_m_robe_extrav_1c_gnd.nif -Decoding meshes\c\c_m_robe_common_5_gnd.nif -Decoding meshes\c\c_m_robe_extrav_1t.1st.nif -Decoding meshes\c\c_m_robe_extrav_1r.1st.nif -Decoding meshes\c\c_m_robe_extrav_1a.1st.nif -Decoding meshes\c\c_m_robe_extrav_1b.1st.nif -Decoding meshes\c\c_m_robe_extrav_1c.1st.nif -Decoding meshes\c\c_m_robe_extrav_1h.1st.nif -Decoding meshes\c\c_m_robe_expensive_2a.nif -Decoding meshes\c\c_m_robe_extrav_1.1st.nif -Decoding meshes\c\c_m_robe_exquisite_1.1st.nif -Decoding meshes\c\c_m_robe_common_02hh_gnd.nif -Decoding meshes\c\c_m_robe_common_02tt.1st.nif -Decoding meshes\c\c_m_robe_common_02rr.1st.nif -Decoding meshes\c\c_m_robe_common_02hh.1st.nif -Decoding meshes\c\c_m_robe_exquisite_1_gnd.nif -Decoding meshes\c\c_m_robe_common_05a.1st.nif -Decoding meshes\c\c_m_robe_common_05b.1st.nif -Decoding meshes\c\c_m_robe_common_05c.1st.nif -Decoding meshes\c\c_m_robe_extrav_1_c.1st.nif -Decoding meshes\c\c_m_robe_common_03b_gnd.nif -Decoding meshes\c\c_m_robe_common_03a_gnd.nif -Decoding meshes\c\c_m_robe_expensive_1_gnd.nif -Decoding meshes\c\c_m_robe_common_05a_gnd.nif -Decoding meshes\c\c_m_robe_common_05b_gnd.nif -Decoding meshes\c\c_m_robe_common_05c_gnd.nif -Decoding meshes\c\c_m_robe_common_02tt_gnd.nif -Decoding meshes\c\c_m_robe_common_03b.1st.nif -Decoding meshes\c\c_m_robe_common_03a.1st.nif -Decoding meshes\c\c_m_robe_expensive_1.1st.nif -Decoding meshes\c\c_m_robe_expensive_2.1st.nif -Decoding meshes\c\c_m_robe_expensive_2_gnd.nif -Decoding meshes\c\c_m_robe_common_02h.1st.nif -Decoding meshes\c\c_m_robe_common_02t.1st.nif -Decoding meshes\c\c_m_robe_common_02r.1st.nif -Decoding meshes\c\c_m_robe_common_02rr_gnd.nif -Decoding meshes\c\c_m_robe_common_02h_gnd.nif -Decoding meshes\c\c_m_robe_common_02r_gnd.nif -Decoding meshes\c\c_m_robe_common_02t_gnd.nif -Decoding meshes\m\misc_argonianhead_01.nif -Decoding meshes\lavasteam.nif -Decoding meshes\left_arrow.nif -Decoding meshes\f\furn_ex_ashl_guarskin.nif -Decoding meshes\l\light_fire.nif -Decoding meshes\lower_arrow.nif -Decoding meshes\lava_sparks.nif -Decoding meshes\c\c_m_robe_expensive_2a_gnd.nif -Decoding meshes\c\c_m_robe_expensive_2a.1st.nif -Decoding meshes\b\b_n_khajiit_m_ankle.nif -Decoding meshes\b\b_n_khajiit_f_ankle.nif -Decoding meshes\b\b_n_khajiit_m_hair02.nif -Decoding meshes\b\b_n_khajiit_f_hair02.nif -Decoding meshes\b\b_n_khajiit_m_hair04.nif -Decoding meshes\b\b_n_khajiit_f_hair04.nif -Decoding meshes\b\b_n_khajiit_f_groin.nif -Decoding meshes\b\b_n_khajiit_m_groin.nif -Decoding meshes\b\b_n_khajiit_m_wrist.nif -Decoding meshes\b\b_n_khajiit_f_wrist.nif -Decoding meshes\b\b_n_khajiit_f_skins.nif -Decoding meshes\b\b_n_khajiit_m_skins.nif -Decoding meshes\b\b_n_khajiit_f_hair03.nif -Decoding meshes\b\b_n_khajiit_m_hair03.nif -Decoding meshes\b\b_n_khajiit_m_hair05.nif -Decoding meshes\b\b_n_khajiit_f_hair05.nif -Decoding meshes\b\b_n_khajiit_f_hair01.nif -Decoding meshes\b\b_n_khajiit_m_hair01.nif -Decoding meshes\b\b_n_khajiit_f_forearm.nif -Decoding meshes\b\b_n_khajiit_f_head_04.nif -Decoding meshes\b\b_n_khajiit_m_head_01.nif -Decoding meshes\b\b_n_khajiit_m_head_03.nif -Decoding meshes\b\b_n_khajiit_m_forearm.nif -Decoding meshes\b\b_n_khajiit_m_head_04.nif -Decoding meshes\b\b_n_khajiit_f_head_03.nif -Decoding meshes\b\b_n_khajiit_f_head_02.nif -Decoding meshes\b\b_n_khajiit_m_head_02.nif -Decoding meshes\b\b_n_khajiit_f_head_01.nif -Decoding meshes\b\b_n_khajiit_f_upper arm.nif -Decoding meshes\b\b_n_khajiit_m_upper arm.nif -Decoding meshes\b\b_n_khajiit_m_upper leg.nif -Decoding meshes\b\b_n_khajiit_f_upper leg.nif -Decoding meshes\b\b_n_khajiit_f_hands.1st.nif -Decoding meshes\b\b_n_khajiit_m_hands.1st.nif -Decoding meshes\o\flora_marshmerrow_02.nif -Decoding meshes\o\flora_marshmerrow_03.nif -Decoding meshes\o\flora_marshmerrow_01.nif -Decoding meshes\f\furn_pycave_spout00.nif -Decoding meshes\m\gold_100.nif -Decoding meshes\m\gold_025.nif -Decoding meshes\m\gold_010.nif -Decoding meshes\m\gold_001.nif -Decoding meshes\m\gold_005.nif -Decoding meshes\menu_help.nif -Decoding meshes\menu_main.nif -Decoding meshes\menu_book.nif -Decoding meshes\marker_light.nif -Decoding meshes\marker_north.nif -Decoding meshes\marker_error.nif -Decoding meshes\marker_arrow.nif -Decoding meshes\m\misc_quill.nif -Decoding meshes\menu_scroll.nif -Decoding meshes\menu_target.nif -Decoding meshes\a\a_bonemold_cuirass_c.nif -Decoding meshes\a\a_bonemold_bracer_w.nif -Decoding meshes\a\a_bonemold_boot_gnd.nif -Decoding meshes\a\a_bonemold_greaves_g.nif -Decoding meshes\a\a_bonemold_greaves_k.nif -Decoding meshes\a\a_bonemold_pauldron_fa.nif -Decoding meshes\a\a_bonemold_pauldron_ua.nif -Decoding meshes\a\a_bonemold_cuirass_gnd.nif -Decoding meshes\a\a_bonemold_gah_julan_c.nif -Decoding meshes\a\a_bonemold_armun_an_ua.nif -Decoding meshes\a\a_bonemold_greaves_gnd.nif -Decoding meshes\a\a_bonemold_bracer_gnd.nif -Decoding meshes\a\a_bonemold_armun_an_cl.nif -Decoding meshes\a\a_bonemold_greaves_ul.nif -Decoding meshes\a\a_bonemold_gah_julan_h.nif -Decoding meshes\a\a_bonemold_gah_j_ua_gnd.nif -Decoding meshes\a\a_bonemold_pauldron_gnd.nif -Decoding meshes\a\a_bonemold_armun_an_helm.nif -Decoding meshes\a\a_bonemold_gah_julan_cl.nif -Decoding meshes\a\a_bonemold_gah_julan_ua.nif -Decoding meshes\a\a_bonemold_armun_ua_gnd.nif -Decoding meshes\a\a_bonemold_chuzei_helmet.nif -Decoding meshes\d\door_redoran_tower_01.nif -Decoding meshes\a\a_bonemold_gah_julan_cgnd.nif -Decoding meshes\c\c_m_bracer_w_leather01.nif -Decoding meshes\b\b_n_orc_m_upper leg.nif -Decoding meshes\b\b_n_orc_f_upper leg.nif -Decoding meshes\b\b_n_orc_m_hands.1st.nif -Decoding meshes\b\b_n_orc_f_hands.1st.nif -Decoding meshes\b\b_n_orc_f_upper arm.nif -Decoding meshes\b\b_n_orc_m_upper arm.nif -Decoding meshes\c\c_m_bracer_w_clothwrap02.nif -Decoding meshes\l\light_de_lantern_08.nif -Decoding meshes\l\light_de_lantern_09.nif -Decoding meshes\l\light_de_lantern_04.nif -Decoding meshes\l\light_de_lantern_14.nif -Decoding meshes\l\light_de_lantern_05.nif -Decoding meshes\l\light_de_lantern_06.nif -Decoding meshes\l\light_de_lantern_07.nif -Decoding meshes\l\light_de_lantern_10.nif -Decoding meshes\l\light_de_lantern_01.nif -Decoding meshes\l\light_de_lantern_11.nif -Decoding meshes\l\light_de_lantern_02.nif -Decoding meshes\l\light_de_lantern_12.nif -Decoding meshes\l\light_de_lantern_03.nif -Decoding meshes\l\light_de_lantern_13.nif -Decoding meshes\l\light_dae_brazier00.nif -Decoding meshes\l\light_dwrv_neonbroke00.nif -Decoding meshes\l\light_de_candle_red_01.nif -Decoding meshes\l\light_de_candle_red_02.nif -Decoding meshes\l\light_de_candle_green_01.nif -Decoding meshes\l\light_de_streetlight_01.nif -Decoding meshes\l\light_de_candle_blue_01.nif -Decoding meshes\l\light_de_candle_ivory_01.nif -Decoding meshes\l\light_de_candle_blue_02.nif -Decoding meshes\o\contain_de_closet_02.nif -Decoding meshes\o\contain_com_chest_01.nif -Decoding meshes\o\contain_egg_kwama00.nif -Decoding meshes\o\contain_tramaroot_05.nif -Decoding meshes\o\contain_tramaroot_02.nif -Decoding meshes\o\contain_pot_blue_02.nif -Decoding meshes\o\contain_pot_blue_01.nif -Decoding meshes\o\contain_tramaroot_03.nif -Decoding meshes\o\contain_com_chest_02.nif -Decoding meshes\o\contain_ropecage_01.nif -Decoding meshes\o\contain_com_hutch_01.nif -Decoding meshes\o\contain_dwrv_desk00.nif -Decoding meshes\o\contain_de_closet_01.nif -Decoding meshes\o\contain_de_table_02.nif -Decoding meshes\o\contain_de_table_01.nif -Decoding meshes\o\contain_tramaroot_06.nif -Decoding meshes\o\contain_com_sack_02.nif -Decoding meshes\o\contain_com_sack_03.nif -Decoding meshes\o\contain_com_sack_01.nif -Decoding meshes\o\contain_tramaroot_01.nif -Decoding meshes\o\contain_dwrv_table00.nif -Decoding meshes\o\contain_de_chest_02.nif -Decoding meshes\o\contain_de_chest_01.nif -Decoding meshes\o\contain_tramaroot_04.nif -Decoding meshes\o\contain_dwrv_chest10.nif -Decoding meshes\o\contain_dwrv_chest00.nif -Decoding meshes\o\contain_dwrv_drawers00.nif -Decoding meshes\o\contain_de_drawers_01.nif -Decoding meshes\o\contain_rock_ebony_03.nif -Decoding meshes\o\contain_cavern_spore00.nif -Decoding meshes\o\contain_pot_redware_01.nif -Decoding meshes\o\contain_rock_glass_03.nif -Decoding meshes\o\contain_rock_glass_04.nif -Decoding meshes\o\contain_ropesphere_01.nif -Decoding meshes\o\contain_trama_shrub_06.nif -Decoding meshes\o\contain_trama_shrub_04.nif -Decoding meshes\o\contain_trama_shrub_02.nif -Decoding meshes\o\contain_rock_glass_01.nif -Decoding meshes\o\contain_rock_ebony_04.nif -Decoding meshes\o\contain_rock_ebony_07.nif -Decoding meshes\o\contain_rock_ebony_06.nif -Decoding meshes\o\contain_com_basket_01.nif -Decoding meshes\o\contain_com_closet_01.nif -Decoding meshes\o\contain_de_crate_logo.nif -Decoding meshes\o\contain_rock_glass_05.nif -Decoding meshes\o\contain_rock_ebony_05.nif -Decoding meshes\o\contain_pot_mottled_01.nif -Decoding meshes\o\contain_chest_small_02.nif -Decoding meshes\o\contain_rock_ebony_02.nif -Decoding meshes\o\contain_rock_glass_07.nif -Decoding meshes\o\contain_rock_ebony_01.nif -Decoding meshes\o\contain_dwrv_closet00.nif -Decoding meshes\o\contain_trama_shrub_05.nif -Decoding meshes\o\contain_trama_shrub_03.nif -Decoding meshes\o\contain_trama_shrub_01.nif -Decoding meshes\o\contain_chest_large_01.nif -Decoding meshes\o\contain_com_drawers_01.nif -Decoding meshes\o\contain_de_drawers_02.nif -Decoding meshes\o\contain_dwrv_barrel10.nif -Decoding meshes\o\contain_dwrv_barrel00.nif -Decoding meshes\o\contain_rock_glass_06.nif -Decoding meshes\o\contain_ropesphere_02.nif -Decoding meshes\o\contain_rock_glass_02.nif -Decoding meshes\o\contain_chest_small_01.nif -Decoding meshes\o\contain_rock_diamond_01.nif -Decoding meshes\o\contain_rock_diamond_03.nif -Decoding meshes\o\contain_rock_diamond_05.nif -Decoding meshes\o\contain_rock_diamond_07.nif -Decoding meshes\o\contain_rock_diamond_02.nif -Decoding meshes\o\contain_rock_diamond_04.nif -Decoding meshes\o\contain_rock_diamond_06.nif -Decoding meshes\o\contain_com_cupboard_01.nif -Decoding meshes\o\lootbag.nif -Decoding meshes\x\furn_de_lightpost_01.nif -Decoding meshes\c\c_m_pants_extrav_2_g.nif -Decoding meshes\c\c_m_pants_extrav_1_g.nif -Decoding meshes\c\c_m_pants_extrav_1_k.nif -Decoding meshes\c\c_m_pants_common_3_k.nif -Decoding meshes\c\c_m_pants_common_5_k.nif -Decoding meshes\c\c_m_pants_g_common00.nif -Decoding meshes\c\c_m_pants_common_5_g.nif -Decoding meshes\c\c_m_pants_common_3_g.nif -Decoding meshes\c\c_m_pants_a_common00.nif -Decoding meshes\c\c_m_pants_k_common00.nif -Decoding meshes\c\c_m_pants_k_common02.nif -Decoding meshes\c\c_m_pants_a_common02.nif -Decoding meshes\c\c_m_pants_g_common02.nif -Decoding meshes\c\c_m_pants_k_common01.nif -Decoding meshes\c\c_m_pants_g_common01.nif -Decoding meshes\c\c_m_pants_a_common01.nif -Decoding meshes\c\c_m_pants_extrav_2_k.nif -Decoding meshes\c\c_m_pants_extrav_1_a.nif -Decoding meshes\c\c_m_pants_extrav_2_a.nif -Decoding meshes\c\c_m_pants_common_5_a.nif -Decoding meshes\c\c_m_pants_common_3_a.nif -Decoding meshes\c\c_m_pants_expens_3_a.nif -Decoding meshes\c\c_m_pants_common_3c_k.nif -Decoding meshes\c\c_m_pants_extrav_1_ul.nif -Decoding meshes\c\c_m_pants_extrav_2_ul.nif -Decoding meshes\c\c_m_pants_ul_common00.nif -Decoding meshes\c\c_m_pants_common_3_ul.nif -Decoding meshes\c\c_m_pants_common_5_ul.nif -Decoding meshes\c\c_m_pants_common_3c_g.nif -Decoding meshes\c\c_m_pants_common_3b_g.nif -Decoding meshes\c\c_m_pants_ul_common02.nif -Decoding meshes\c\c_m_pants_gnd_common02.nif -Decoding meshes\c\c_m_pants_gnd_common00.nif -Decoding meshes\c\c_m_pants_ul_common01.nif -Decoding meshes\c\c_m_pants_common_3b_a.nif -Decoding meshes\c\c_m_pants_common_3c_a.nif -Decoding meshes\c\c_m_pants_expens_1_e_g.nif -Decoding meshes\c\c_m_pants_expens_1_e_a.nif -Decoding meshes\c\c_m_pants_expens_1_e_k.nif -Decoding meshes\c\c_m_pants_expens_1_a_g.nif -Decoding meshes\c\c_m_pants_expens_1_a_a.nif -Decoding meshes\c\c_m_pants_expens_1_a_k.nif -Decoding meshes\c\c_m_pants_expens_1_z_g.nif -Decoding meshes\c\c_m_pants_expens_1_z_a.nif -Decoding meshes\c\c_m_pants_expens_1_z_k.nif -Decoding meshes\c\c_m_pants_common_1_z_k.nif -Decoding meshes\c\c_m_pants_common_1_z_a.nif -Decoding meshes\c\c_m_pants_common_1_z_g.nif -Decoding meshes\c\c_m_pants_common_1_u_k.nif -Decoding meshes\c\c_m_pants_common_1_u_a.nif -Decoding meshes\c\c_m_pants_common_1_u_g.nif -Decoding meshes\c\c_m_pants_common_1_e_k.nif -Decoding meshes\c\c_m_pants_common_1_e_a.nif -Decoding meshes\c\c_m_pants_common_1_e_g.nif -Decoding meshes\c\c_m_pants_common_1_a_k.nif -Decoding meshes\c\c_m_pants_common_1_a_a.nif -Decoding meshes\c\c_m_pants_common_1_a_g.nif -Decoding meshes\c\c_m_pants_common_4_b_a.nif -Decoding meshes\c\c_m_pants_common_4_b_g.nif -Decoding meshes\c\c_m_pants_common_4_b_k.nif -Decoding meshes\c\c_m_pants_common_3b_k.nif -Decoding meshes\c\c_m_pants_common_5_gnd.nif -Decoding meshes\c\c_m_pants_common_3_gnd.nif -Decoding meshes\c\c_m_pants_extrav_1_gnd.nif -Decoding meshes\c\c_m_pants_extrav_2_gnd.nif -Decoding meshes\c\c_m_pants_common_3c_ul.nif -Decoding meshes\c\c_m_pants_common_3b_ul.nif -Decoding meshes\c\c_m_pants_gnd_common01.nif -Decoding meshes\c\c_m_pants_exquisite_1g.nif -Decoding meshes\c\c_m_pants_expens_3_gnd.nif -Decoding meshes\c\c_m_pants_common_4_b_ul.nif -Decoding meshes\c\c_m_pants_expens_1_a_ul.nif -Decoding meshes\c\c_m_pants_expens_1_e_ul.nif -Decoding meshes\c\c_m_pants_expens_1_u_ul.nif -Decoding meshes\c\c_m_pants_expens_1_z_ul.nif -Decoding meshes\c\c_m_pants_expens_1_z_gnd.nif -Decoding meshes\c\c_m_pants_expensive_1_k.nif -Decoding meshes\c\c_m_pants_expensive_1_g.nif -Decoding meshes\c\c_m_pants_expensive_1_a.nif -Decoding meshes\c\c_m_pants_expensive_2_k.nif -Decoding meshes\c\c_m_pants_expensive_2_g.nif -Decoding meshes\c\c_m_pants_expensive_2_a.nif -Decoding meshes\c\c_m_pants_exquisite_1_ul.nif -Decoding meshes\c\c_m_pants_common_1_u_gnd.nif -Decoding meshes\c\c_m_pants_common_1_a_ul.nif -Decoding meshes\c\c_m_pants_common_1_e_ul.nif -Decoding meshes\c\c_m_pants_common_1_z_ul.nif -Decoding meshes\c\c_m_pants_common_1_u_ul.nif -Decoding meshes\c\c_m_pants_common_1_z_gnd.nif -Decoding meshes\c\c_m_pants_exquisite_1_k.nif -Decoding meshes\c\c_m_pants_exquisite_1_g.nif -Decoding meshes\c\c_m_pants_exquisite_1_a.nif -Decoding meshes\c\c_m_pants_expens_1_u_gnd.nif -Decoding meshes\c\c_m_pants_common_4_b_gnd.nif -Decoding meshes\c\c_m_pants_common_3c_gnd.nif -Decoding meshes\c\c_m_pants_common_3b_gnd.nif -Decoding meshes\c\c_m_pants_common_1_a_gnd.nif -Decoding meshes\c\c_m_pants_common_1_e_gnd.nif -Decoding meshes\c\c_m_pants_exqisite_1_gnd.nif -Decoding meshes\c\c_m_pants_expens_1_a_gnd.nif -Decoding meshes\c\c_m_pants_expensive_1_ul.nif -Decoding meshes\c\c_m_pants_expensive_2_ul.nif -Decoding meshes\c\c_m_pants_expens_1_e_gnd.nif -Decoding meshes\a\a_dwemer_cl_pauldron.nif -Decoding meshes\a\a_dwemer_pauldron_ua.nif -Decoding meshes\a\a_dwemer_pauldron_fa.nif -Decoding meshes\a\a_dwemer_greaves_gnd.nif -Decoding meshes\a\a_dwemer_greaves_ul.nif -Decoding meshes\a\a_dwemer_pauldron_gnd.nif -Decoding meshes\a\a_dwemer_bracer_w_gnd.nif -Decoding meshes\i\xin_dagoth_bridge00.nif -Decoding meshes\m\text_quarto_open_04.nif -Decoding meshes\m\text_quarto_open_01.nif -Decoding meshes\m\text_quarto_open_03.nif -Decoding meshes\m\text_quarto_open_02.nif -Decoding meshes\c\c_m_pants_expensive_1_u_ul.nif -Decoding meshes\c\c_m_pants_exquisite_1_gnd.nif -Decoding meshes\c\c_m_pants_expensive_1_u_k.nif -Decoding meshes\c\c_m_pants_expensive_1_u_a.nif -Decoding meshes\c\c_m_pants_expensive_1_u_g.nif -Decoding meshes\c\c_m_pants_expensive_1_gnd.nif -Decoding meshes\c\c_m_pants_expensive_2_gnd.nif -Decoding meshes\f\furn_6th_bellhammer.nif -Decoding meshes\f\furn_6th_tallbanner.nif -Decoding meshes\f\furn_6th_dagothsymbol.nif -Decoding meshes\f\furn_6th_corpus_plate_01.nif -Decoding meshes\w\w_silver_shortsword.nif -Decoding meshes\c\c_glove_common1.1st.nif -Decoding meshes\c\c_glove_balmolagmer.nif -Decoding meshes\c\c_glove_extravagant1.nif -Decoding meshes\c\c_glove_common1_gnd.nif -Decoding meshes\c\c_glove_expensive1.1st.nif -Decoding meshes\c\c_glove_moragtong.1st.nif -Decoding meshes\c\c_glove_moragtong_gnd.nif -Decoding meshes\c\c_glove_expensive1_gnd.nif -Decoding meshes\c\c_glove_balmolagmer_gnd.nif -Decoding meshes\c\c_glove_balmolagmer.1st.nif -Decoding meshes\c\c_glove_extravagant1_gnd.nif -Decoding meshes\c\c_glove_extravagant1.1st.nif -Decoding meshes\e\magic_cast_levitate.nif -Decoding meshes\f\xfurn_redoran_flag_01.nif -Decoding meshes\c\c_m_shoe_f_leather00.nif -Decoding meshes\c\c_m_shoe_f_leather01.nif -Decoding meshes\c\c_m_shoe_common01_gnd.nif -Decoding meshes\c\c_m_shoe_expensive_1_f.nif -Decoding meshes\c\c_m_shoe_common02_gnd.nif -Decoding meshes\c\c_m_shoe_expens_1_gnd.nif -Decoding meshes\w\w_warhammer_daedric.nif -Decoding meshes\a\a_art_apostle_boots_r.nif -Decoding meshes\ashcloud.nif -Decoding meshes\w\w_art_warhammer_crusher.nif -Decoding meshes\a\a_art_apostle_boots_gnd.nif -Decoding meshes\f\furn_mudcave_pool00.nif -Decoding meshes\f\furn_mudcave_spout00.nif -Decoding meshes\f\furn_cushion_round_07.nif -Decoding meshes\f\furn_cushion_round_03.nif -Decoding meshes\f\furn_cushion_round_04.nif -Decoding meshes\f\furn_cushion_square_03.nif -Decoding meshes\f\furn_cushion_square_01.nif -Decoding meshes\f\furn_cushion_square_07.nif -Decoding meshes\f\furn_cushion_square_05.nif -Decoding meshes\f\furn_cushion_square_09.nif -Decoding meshes\f\furn_cushion_round_02.nif -Decoding meshes\f\furn_cushion_round_01.nif -Decoding meshes\f\furn_cushion_round_06.nif -Decoding meshes\f\furn_cushion_square_02.nif -Decoding meshes\f\furn_cushion_square_06.nif -Decoding meshes\f\furn_cushion_square_04.nif -Decoding meshes\f\furn_cushion_square_08.nif -Decoding meshes\f\furn_cushion_round_05.nif -Decoding meshes\f\furn_rug_big_04_collision.nif -Decoding meshes\a\a_dreugh_cuirass_gnd.nif -Decoding meshes\o\flora_bittergreen_07.nif -Decoding meshes\o\flora_bittergreen_02.nif -Decoding meshes\o\flora_bc_podplant_04.nif -Decoding meshes\o\flora_bittergreen_03.nif -Decoding meshes\o\flora_bittergreen_08.nif -Decoding meshes\o\flora_bittergreen_09.nif -Decoding meshes\o\flora_bittergreen_04.nif -Decoding meshes\o\flora_bittergreen_06.nif -Decoding meshes\o\flora_bittergreen_01.nif -Decoding meshes\o\flora_bittergreen_10.nif -Decoding meshes\o\flora_bittergreen_05.nif -Decoding meshes\o\flora_black_lichen_02.nif -Decoding meshes\o\flora_black_anther_01.nif -Decoding meshes\o\flora_black_lichen_03.nif -Decoding meshes\o\flora_black_anther_02.nif -Decoding meshes\o\flora_black_lichen_01.nif -Decoding meshes\o\flora_bittergreen_pod_01.nif -Decoding meshes\button.nif -Decoding meshes\base_anim.nif -Decoding meshes\bloodsplat.nif -Decoding meshes\bloodsplat2.nif -Decoding meshes\bloodsplat3.nif -Decoding meshes\blightcloud.nif -Decoding meshes\base_animkna.nif -Decoding meshes\m\probe_apprentice_01.nif -Decoding meshes\c\c_shoes_extrav_2_gnd.nif -Decoding meshes\c\c_shoes_common_4_gnd.nif -Decoding meshes\c\c_shoes_common_3_gnd.nif -Decoding meshes\c\c_shoes_expensive_2.nif -Decoding meshes\c\c_shoes_extrav_1_gnd.nif -Decoding meshes\c\c_shoes_common_5_gnd.nif -Decoding meshes\c\c_shoes_expensive_03.nif -Decoding meshes\c\c_shoes_exquisite_1_f.nif -Decoding meshes\c\c_shoes_expensive_2_gnd.nif -Decoding meshes\c\c_shoes_expensive_3_gnd.nif -Decoding meshes\c\c_shoes_exquisite_1_gnd.nif -Decoding meshes\l\light_hangingl_blue_01.nif -Decoding meshes\l\light_hangings_blue_01.nif -Decoding meshes\x\flora_t_mushroom_02.nif -Decoding meshes\x\flora_t_mushroom_01.nif -Decoding meshes\x\flora_t_shelffungus_01.nif -Decoding meshes\b\b_n_wood elf_m_neck.nif -Decoding meshes\b\b_n_wood elf_f_neck.nif -Decoding meshes\b\b_n_wood elf_f_skins.nif -Decoding meshes\b\b_n_wood elf_m_skins.nif -Decoding meshes\b\b_n_wood elf_m_foot.nif -Decoding meshes\b\b_n_wood elf_m_wrist.nif -Decoding meshes\b\b_n_wood elf_m_knee.nif -Decoding meshes\b\b_n_wood elf_f_knee.nif -Decoding meshes\b\b_n_wood elf_f_groin.nif -Decoding meshes\b\b_n_wood elf_m_groin.nif -Decoding meshes\b\b_n_wood elf_f_wrist.nif -Decoding meshes\b\b_n_wood elf_f_foot.nif -Decoding meshes\b\b_n_wood elf_m_ankle.nif -Decoding meshes\b\b_n_wood elf_f_ankle.nif -Decoding meshes\b\b_n_wood elf_f_head_02.nif -Decoding meshes\b\b_n_wood elf_f_head_06.nif -Decoding meshes\b\b_n_wood elf_f_head_04.nif -Decoding meshes\b\b_n_wood elf_m_head_02.nif -Decoding meshes\b\b_n_wood elf_m_head_06.nif -Decoding meshes\b\b_n_wood elf_m_head_04.nif -Decoding meshes\b\b_n_wood elf_m_head_08.nif -Decoding meshes\b\b_n_wood elf_f_hair_05.nif -Decoding meshes\b\b_n_wood elf_f_hair_01.nif -Decoding meshes\b\b_n_wood elf_f_hair_03.nif -Decoding meshes\b\b_n_wood elf_m_hair_05.nif -Decoding meshes\b\b_n_wood elf_m_hair_01.nif -Decoding meshes\b\b_n_wood elf_m_hair_03.nif -Decoding meshes\b\b_n_wood elf_m_forearm.nif -Decoding meshes\b\b_n_wood elf_f_head_03.nif -Decoding meshes\b\b_n_wood elf_f_head_01.nif -Decoding meshes\b\b_n_wood elf_f_head_05.nif -Decoding meshes\b\b_n_wood elf_m_head_03.nif -Decoding meshes\b\b_n_wood elf_m_head_01.nif -Decoding meshes\b\b_n_wood elf_m_head_07.nif -Decoding meshes\b\b_n_wood elf_m_head_05.nif -Decoding meshes\b\b_n_wood elf_f_hair_04.nif -Decoding meshes\b\b_n_wood elf_f_hair_02.nif -Decoding meshes\b\b_n_wood elf_m_hair_04.nif -Decoding meshes\b\b_n_wood elf_m_hair_06.nif -Decoding meshes\b\b_n_wood elf_m_hair_02.nif -Decoding meshes\b\b_n_wood elf_f_forearm.nif -Decoding meshes\b\b_n_wood elf_m_upper arm.nif -Decoding meshes\b\b_n_wood elf_f_upper leg.nif -Decoding meshes\b\b_n_wood elf_m_upper leg.nif -Decoding meshes\b\b_n_wood elf_f_hands.1st.nif -Decoding meshes\b\b_n_wood elf_m_hands.1st.nif -Decoding meshes\b\b_n_wood elf_f_upper arm.nif -Decoding meshes\x\terraiin_rock_ma_07.nif -Decoding meshes\x\terraiin_rock_rm_07.nif -Decoding meshes\a\a_art_cuirass_lords_c.nif -Decoding meshes\a\a_art_cuirass_savior_c.nif -Decoding meshes\cursor.nif -Decoding meshes\cursormove.nif -Decoding meshes\a\a_art_cuirass_lords_gnd.nif -Decoding meshes\a\a_art_cuirass_savior_gnd.nif -Decoding meshes\f\furn_dwrv_fitting50.nif -Decoding meshes\f\furn_dwrv_fitting40.nif -Decoding meshes\f\furn_dwrv_fitting10.nif -Decoding meshes\f\furn_dwrv_fitting00.nif -Decoding meshes\f\furn_dwrv_fitting30.nif -Decoding meshes\f\furn_dwrv_fitting20.nif -Decoding meshes\f\furn_dwrv_cabinet00.nif -Decoding meshes\f\furn_dwrv_beltdrive00.nif -Decoding meshes\f\furn_dwrv_bookshelf00.nif -Decoding meshes\cursor_drop.nif -Decoding meshes\c\c_skirt_exquisite_1.nif -Decoding meshes\c\c_skirt_common_3_gnd.nif -Decoding meshes\c\c_skirt_common_2_gnd.nif -Decoding meshes\c\c_skirt_expensive_1.nif -Decoding meshes\c\c_skirt_expensive_3.nif -Decoding meshes\c\c_skirt_expensive_2.nif -Decoding meshes\c\c_skirt_common_5_gnd.nif -Decoding meshes\c\c_skirt_extravagant_1.nif -Decoding meshes\c\c_skirt_extravagant_2.nif -Decoding meshes\c\c_skirt_extravagant_1gnd.nif -Decoding meshes\c\c_skirt_extravagant_2gnd.nif -Decoding meshes\c\c_skirt_expensive_2_gnd.nif -Decoding meshes\c\c_skirt_expensive_3_gnd.nif -Decoding meshes\c\c_skirt_expensive_1_gnd.nif -Decoding meshes\c\c_skirt_exquisite_1_gnd.nif -Decoding meshes\w\magic_target_poison.nif -Decoding meshes\w\magic_target_conjure.nif -Decoding meshes\f\flora_muckspunge_06.nif -Decoding meshes\f\flora_muckspunge_07.nif -Decoding meshes\f\flora_muckspunge_04.nif -Decoding meshes\f\flora_muckspunge_05.nif -Decoding meshes\f\flora_muckspunge_02.nif -Decoding meshes\f\flora_muckspunge_03.nif -Decoding meshes\f\flora_muckspunge_01.nif -Decoding meshes\b\b_v_imperial_m_head_01.nif -Decoding meshes\b\b_v_imperial_f_head_01.nif -Decoding meshes\x\terrain_rock_ma_550.nif -Decoding meshes\x\terrain_lava_ventlg.nif -Decoding meshes\x\terrain_rock_ma_arch.nif -Decoding meshes\x\terrain_lava_ventlg01.nif -Decoding meshes\x\terrain_ashland_rock_16.nif -Decoding meshes\x\terrain_ashland_rock_14.nif -Decoding meshes\x\terrain_ashland_rock_12.nif -Decoding meshes\x\terrain_ashland_rock_10.nif -Decoding meshes\x\terrain_ashland_rock_08.nif -Decoding meshes\x\terrain_ashland_rock_04.nif -Decoding meshes\x\terrain_ashland_rock_06.nif -Decoding meshes\x\terrain_ashland_rock_02.nif -Decoding meshes\x\terrain_ashland_rock_15.nif -Decoding meshes\x\terrain_ashland_rock_13.nif -Decoding meshes\x\terrain_ashland_rock_11.nif -Decoding meshes\x\terrain_ashland_rock_09.nif -Decoding meshes\x\terrain_ashland_rock_05.nif -Decoding meshes\x\terrain_ashland_rock_07.nif -Decoding meshes\x\terrain_ashland_rock_01.nif -Decoding meshes\x\terrain_ashland_rock_03.nif -Decoding meshes\f\furn_spinningwheel_01.nif -Decoding meshes\f\xact_banner_hla_oad.nif -Decoding meshes\f\xact_banner_tel_fyr.nif -Decoding meshes\f\xact_banner_tel_mora.nif -Decoding meshes\f\xact_banner_tel_vos.nif -Decoding meshes\f\xact_banner_tel_aruhn.nif -Decoding meshes\f\xact_banner_gnaar_mok.nif -Decoding meshes\f\xact_banner_sadrith_mora.nif -Decoding meshes\f\xact_banner_ald_velothi.nif -Decoding meshes\f\xact_banner_tel_branora.nif -Decoding meshes\a\a_art_dragon_cuirass_c.nif -Decoding meshes\d\ex_dae_door_load_oval.nif -Decoding meshes\d\scene root.nif -Decoding meshes\c\c_skirt_extravagant_2_gnd.nif -Decoding meshes\c\c_skirt_extravagant_1_gnd.nif -Decoding meshes\m\probe_grandmaster_01.nif -Decoding meshes\a\a_m_chitin_hands.1st.nif -Decoding meshes\a\a_m_chitin_g_greaves.nif -Decoding meshes\a\a_m_chitin_boot_gnd.nif -Decoding meshes\a\a_m_chitin_ua_pauldron.nif -Decoding meshes\a\a_m_chitin_greaves_gnd.nif -Decoding meshes\a\a_m_chitin_ul_greaves.nif -Decoding meshes\a\a_m_chitin_cuirass_gnd.nif -Decoding meshes\a\a_m_chitin_pauldron_cl.nif -Decoding meshes\a\a_m_chitin_gauntlet_gnd.nif -Decoding meshes\a\a_art_ebon_cuirass_c.nif -Decoding meshes\a\a_art_ebon_cuirass_gnd.nif -Decoding meshes\w\w_art_staff_hasedoki.nif -Decoding meshes\e\blight.nif -Decoding meshes\e\hand01.nif -Decoding meshes\e\absorb.nif -Decoding meshes\e\cure_hit.nif -Decoding meshes\e\corprus.nif -Decoding meshes\w\shadowshortbladeonehand.nif -Decoding meshes\e\magic_hit.nif -Decoding meshes\e\magic_cast.nif -Decoding meshes\e\magic_area.nif -Decoding meshes\editormarker.nif -Decoding meshes\e\frost_hit.nif -Decoding meshes\b\b_v_dark elf_f_head_01.nif -Decoding meshes\b\b_v_dark elf_m_head_01.nif -Decoding meshes\a\a_nordicfur_skinned.nif -Decoding meshes\a\a_nordicfur_bracer_w.nif -Decoding meshes\a\a_nordicfur_boot_gnd.nif -Decoding meshes\a\a_nordicfur_greave_g.nif -Decoding meshes\a\a_nordicfur_greave_gnd.nif -Decoding meshes\a\a_nordicfur_hands.1st.nif -Decoding meshes\a\a_nordicfur_greave_ul.nif -Decoding meshes\a\a_nordicfur_pauldron_gnd.nif -Decoding meshes\a\a_nordicfur_gauntlet_gnd.nif -Decoding meshes\a\a_nordicfur_pauldron_cl.nif -Decoding meshes\a\a_nordicfur_pauldron_ua.nif -Decoding meshes\f\furn_practice_dummy.nif -Decoding meshes\a\a_art_fists_gauntlets.nif -Decoding meshes\fire_small.nif -Decoding meshes\f\ex_turf00.nif -Decoding meshes\f\furn_web00.nif -Decoding meshes\f\furn_web10.nif -Decoding meshes\f\furn_cot00.nif -Decoding meshes\a\a_art_fists_gauntlets.1st.nif -Decoding meshes\a\a_nordicfur_m_cuirass_gnd.nif -Decoding meshes\a\a_netch_m_greave_ul.nif -Decoding meshes\a\a_netch_m_greave_gnd.nif -Decoding meshes\a\a_netch_m_hands.1st.nif -Decoding meshes\a\a_netch_boiled_helm.nif -Decoding meshes\a\a_netch_m_pauldron_ua.nif -Decoding meshes\a\a_netch_m_pauldron_gnd.nif -Decoding meshes\a\a_netch_m_currais2_gnd.nif -Decoding meshes\a\a_netch_m_cuirass_gnd.nif -Decoding meshes\a\a_netch_m_pauldron_cl.nif -Decoding meshes\a\a_netch_m_gauntlet_gnd.nif -Decoding meshes\c\c_m_shirt_common_3_w.nif -Decoding meshes\c\c_m_shirt_common_5_w.nif -Decoding meshes\c\c_m_shirt_common_5_c.nif -Decoding meshes\c\c_m_shirt_common_3_c.nif -Decoding meshes\c\c_m_shirt_expens_3_w.nif -Decoding meshes\c\c_m_shirt_extrav_1_c.nif -Decoding meshes\c\c_m_shirt_extrav_2_c.nif -Decoding meshes\c\c_m_shirt_c_common01.nif -Decoding meshes\c\c_m_shirt_common_5_f.nif -Decoding meshes\c\c_m_shirt_extrav_1_w.nif -Decoding meshes\c\c_m_shirt_extrav_2_w.nif -Decoding meshes\c\c_m_shirt_c_common03.nif -Decoding meshes\c\c_m_shirt_w_common03.nif -Decoding meshes\c\c_m_shirt_expens_3_c.nif -Decoding meshes\c\c_m_shirt_extrav_1_tfa.nif -Decoding meshes\c\c_m_shirt_extrav_1_rfa.nif -Decoding meshes\c\c_m_shirt_extrav_1_hfa.nif -Decoding meshes\c\c_m_shirt_expens_1_z_f.nif -Decoding meshes\c\c_m_shirt_common_1_z_c.nif -Decoding meshes\c\c_m_shirt_common_1_u_w.nif -Decoding meshes\c\c_m_shirt_common_1_u_c.nif -Decoding meshes\c\c_m_shirt_common_2tt_w.nif -Decoding meshes\c\c_m_shirt_common_2tt_c.nif -Decoding meshes\c\c_m_shirt_common_2_t_w.nif -Decoding meshes\c\c_m_shirt_common_2_t_c.nif -Decoding meshes\c\c_m_shirt_common_2rr_w.nif -Decoding meshes\c\c_m_shirt_common_2rr_c.nif -Decoding meshes\c\c_m_shirt_common_2_r_w.nif -Decoding meshes\c\c_m_shirt_common_2_r_c.nif -Decoding meshes\c\c_m_shirt_common_2hh_w.nif -Decoding meshes\c\c_m_shirt_common_2hh_c.nif -Decoding meshes\c\c_m_shirt_common_2_h_w.nif -Decoding meshes\c\c_m_shirt_common_2_h_c.nif -Decoding meshes\c\c_m_shirt_common_1_e_w.nif -Decoding meshes\c\c_m_shirt_common_1_e_c.nif -Decoding meshes\c\c_m_shirt_common_4_a_w.nif -Decoding meshes\c\c_m_shirt_common_1_a_w.nif -Decoding meshes\c\c_m_shirt_common_4_a_c.nif -Decoding meshes\c\c_m_shirt_common_1_a_c.nif -Decoding meshes\c\c_m_shirt_common_4_c_w.nif -Decoding meshes\c\c_m_shirt_common_4_c_c.nif -Decoding meshes\c\c_m_shirt_common_4_b_w.nif -Decoding meshes\c\c_m_shirt_common_4_b_c.nif -Decoding meshes\c\c_m_shirt_gondalier_c.nif -Decoding meshes\c\c_m_shirt_common_3c_w.nif -Decoding meshes\c\c_m_shirt_extrav_2_ua.nif -Decoding meshes\c\c_m_shirt_extrav_1_ua.nif -Decoding meshes\c\c_m_shirt_extrav_2_fa.nif -Decoding meshes\c\c_m_shirt_extrav_1_fa.nif -Decoding meshes\c\c_m_shirt_extrav_1_tw.nif -Decoding meshes\c\c_m_shirt_fa_commonl02.nif -Decoding meshes\c\c_m_shirt_fa_commonl04.nif -Decoding meshes\c\c_m_shirt_ua_commonl02.nif -Decoding meshes\c\c_m_shirt_ua_commonl04.nif -Decoding meshes\c\c_m_shirt_common_3c_c.nif -Decoding meshes\c\c_m_shirt_common_3b_c.nif -Decoding meshes\c\c_m_shirt_common_3c_ua.nif -Decoding meshes\c\c_m_shirt_common_3b_ua.nif -Decoding meshes\c\c_m_shirt_extrav_1_rw.nif -Decoding meshes\c\c_m_shirt_ua_common01.nif -Decoding meshes\c\c_m_shirt_common_3c_fa.nif -Decoding meshes\c\c_m_shirt_expens_1_e_w.nif -Decoding meshes\c\c_m_shirt_expens_1_e_c.nif -Decoding meshes\c\c_m_shirt_expens_1_a_w.nif -Decoding meshes\c\c_m_shirt_expens_1_z_w.nif -Decoding meshes\c\c_m_shirt_expens_1_z_c.nif -Decoding meshes\c\c_m_shirt_common_1_u_f.nif -Decoding meshes\c\c_m_shirt_common_1_a_f.nif -Decoding meshes\c\c_m_shirt_common_5_gnd.nif -Decoding meshes\c\c_m_shirt_common_3_gnd.nif -Decoding meshes\c\c_m_shirt_extrav_1_hw.nif -Decoding meshes\c\c_m_shirt_common_3_ua.nif -Decoding meshes\c\c_m_shirt_common_5_ua.nif -Decoding meshes\c\c_m_shirt_common_3_fa.nif -Decoding meshes\c\c_m_shirt_extrav_1_hua.nif -Decoding meshes\c\c_m_shirt_expens_3_fa.nif -Decoding meshes\c\c_m_shirt_expens_3_ua.nif -Decoding meshes\c\c_m_shirt_extrav_1_rua.nif -Decoding meshes\c\c_m_shirt_w_commonl04.nif -Decoding meshes\c\c_m_shirt_c_commonl04.nif -Decoding meshes\c\c_m_shirt_extrav_1_gnd.nif -Decoding meshes\c\c_m_shirt_extrav_2_gnd.nif -Decoding meshes\c\c_m_shirt_extrav_1_tua.nif -Decoding meshes\c\c_m_shirt_ua_common03.nif -Decoding meshes\c\c_m_shirt_fa_common03.nif -Decoding meshes\c\c_m_shirt_gnd_common03.nif -Decoding meshes\c\c_m_shirt_gnd_common01.nif -Decoding meshes\c\c_m_shirt_extrav_1_hc.nif -Decoding meshes\c\c_m_shirt_extrav_1_tc.nif -Decoding meshes\c\c_m_shirt_extrav_1_rc.nif -Decoding meshes\c\c_m_shirt_expens_3_gnd.nif -Decoding meshes\c\c_m_shirt_c_commonl02.nif -Decoding meshes\c\c_m_shirt_expens_1_z_gnd.nif -Decoding meshes\c\c_m_shirt_expensive_1_c.nif -Decoding meshes\c\c_m_shirt_expensive_1_w.nif -Decoding meshes\c\c_m_shirt_expensive_2_c.nif -Decoding meshes\c\c_m_shirt_expensive_2_w.nif -Decoding meshes\c\c_m_shirt_exquisite_1_ua.nif -Decoding meshes\c\c_m_shirt_common_1_u_gnd.nif -Decoding meshes\c\c_m_shirt_common_1_z_gnd.nif -Decoding meshes\c\c_m_shirt_common_1_e_fa.nif -Decoding meshes\c\c_m_shirt_exquisite_1_w.nif -Decoding meshes\c\c_m_shirt_exquisite_1_c.nif -Decoding meshes\c\c_m_shirt_exquisite_1_fa.nif -Decoding meshes\c\c_m_shirt_common_2hh_gnd.nif -Decoding meshes\c\c_m_shirt_common_2_h_gnd.nif -Decoding meshes\c\c_m_shirt_expens_1_u_gnd.nif -Decoding meshes\c\c_m_shirt_gondolier_gnd.nif -Decoding meshes\c\c_m_shirt_common_4_c_gnd.nif -Decoding meshes\c\c_m_shirt_common_4_a_ua.nif -Decoding meshes\c\c_m_shirt_common_4_c_ua.nif -Decoding meshes\c\c_m_shirt_common_4_b_ua.nif -Decoding meshes\c\c_m_shirt_common_2tt_gnd.nif -Decoding meshes\c\c_m_shirt_common_2_t_gnd.nif -Decoding meshes\c\c_m_shirt_common_4_b_gnd.nif -Decoding meshes\c\c_m_shirt_expens_1_u_fa.nif -Decoding meshes\c\c_m_shirt_expens_1_e_fa.nif -Decoding meshes\c\c_m_shirt_expens_1_a_fa.nif -Decoding meshes\c\c_m_shirt_expens_1_a_ua.nif -Decoding meshes\c\c_m_shirt_expens_1_e_ua.nif -Decoding meshes\c\c_m_shirt_expens_1_u_ua.nif -Decoding meshes\c\c_m_shirt_expens_1_z_ua.nif -Decoding meshes\c\c_m_shirt_common_4_a_gnd.nif -Decoding meshes\c\c_m_shirt_common_3c_gnd.nif -Decoding meshes\c\c_m_shirt_common_3b_gnd.nif -Decoding meshes\c\c_m_shirt_common_1_a_gnd.nif -Decoding meshes\c\c_m_shirt_expensive_1_fa.nif -Decoding meshes\c\c_m_shirt_expensive_2_fa.nif -Decoding meshes\c\c_m_shirt_common_2_h_fa.nif -Decoding meshes\c\c_m_shirt_common_2_r_fa.nif -Decoding meshes\c\c_m_shirt_common_2_t_fa.nif -Decoding meshes\c\c_m_shirt_common_2rr_gnd.nif -Decoding meshes\c\c_m_shirt_common_2_r_gnd.nif -Decoding meshes\c\c_m_shirt_common_1_e_gnd.nif -Decoding meshes\c\c_m_shirt_gnd_commonl02.nif -Decoding meshes\c\c_m_shirt_gnd_commonl04.nif -Decoding meshes\c\c_m_shirt_common_1_a_ua.nif -Decoding meshes\c\c_m_shirt_common_1_e_ua.nif -Decoding meshes\c\c_m_shirt_common_1_z_ua.nif -Decoding meshes\c\c_m_shirt_common_1_u_ua.nif -Decoding meshes\c\c_m_shirt_common_2hh_fa.nif -Decoding meshes\c\c_m_shirt_extrav_1_hgnd.nif -Decoding meshes\c\c_m_shirt_common_2_t_ua.nif -Decoding meshes\c\c_m_shirt_common_2tt_ua.nif -Decoding meshes\c\c_m_shirt_expens_1_a_gnd.nif -Decoding meshes\c\c_m_shirt_common_2_r_ua.nif -Decoding meshes\c\c_m_shirt_common_2rr_ua.nif -Decoding meshes\c\c_m_shirt_common_2_h_ua.nif -Decoding meshes\c\c_m_shirt_common_2hh_ua.nif -Decoding meshes\c\c_m_shirt_common_4_a_fa.nif -Decoding meshes\c\c_m_shirt_common_4_b_fa.nif -Decoding meshes\c\c_m_shirt_common_4_c_fa.nif -Decoding meshes\c\c_m_shirt_expensive_1_ua.nif -Decoding meshes\c\c_m_shirt_expensive_2_ua.nif -Decoding meshes\c\c_m_shirt_expens_1_e_gnd.nif -Decoding meshes\c\c_m_shirt_common_2rr_fa.nif -Decoding meshes\c\c_m_shirt_extrav_1_tgnd.nif -Decoding meshes\c\c_m_shirt_common_2tt_fa.nif -Decoding meshes\c\c_m_shirt_extrav_1_rgnd.nif -Decoding meshes\c\c_m_skirt_imperial_gnd.nif -Decoding meshes\c\c_m_skirt_templar_gnd.nif -Decoding meshes\c\c_m_skirt_common_01_gnd.nif -Decoding meshes\o\flora_gold_kanet_02.nif -Decoding meshes\o\flora_gold_kanet_01.nif -Decoding meshes\o\flora_green_lichen_02.nif -Decoding meshes\o\flora_green_lichen_03.nif -Decoding meshes\o\flora_green_lichen_01.nif -Decoding meshes\a\a_art_gauntlet_fist_gnd.nif -Decoding meshes\x\furn_imp_rubble_ring.nif -Decoding meshes\f\furn_ashl_bugbowl_01.nif -Decoding meshes\f\furn_ashl_bugbowl_02.nif -Decoding meshes\f\furn_ashl_bugbowl_03.nif -Decoding meshes\f\furn_ashl_chimes_01.nif -Decoding meshes\c\c_m_skirt_common_04_c_gnd.nif -Decoding meshes\c\c_m_shirt_expensive_1_a_c.nif -Decoding meshes\c\c_m_shirt_expensive_1_u_fa.nif -Decoding meshes\c\c_m_shirt_expensive_1_u_ua.nif -Decoding meshes\c\c_m_shirt_exquisite_1_gnd.nif -Decoding meshes\c\c_m_shirt_expensive_1_u_c.nif -Decoding meshes\c\c_m_shirt_expensive_1_u_w.nif -Decoding meshes\c\c_m_shirt_expensive_1_gnd.nif -Decoding meshes\c\c_m_shirt_expensive_2_gnd.nif -Decoding meshes\l\light_spear_skull00.nif -Decoding meshes\c\c_indoril_m_ul_pants.nif -Decoding meshes\c\c_indoril_m_g_pants.nif -Decoding meshes\c\c_indoril_m_k_pants.nif -Decoding meshes\x\ex_imp_plaza_stairs.nif -Decoding meshes\x\ex_imp_guardtower_02.nif -Decoding meshes\x\ex_imp_foundation_01.nif -Decoding meshes\x\ex_imp_towerb_med_01.nif -Decoding meshes\x\ex_imp_towers_med_01.nif -Decoding meshes\x\ex_imp_wall_arch_01.nif -Decoding meshes\x\ex_imp_arrowslit_01.nif -Decoding meshes\x\ex_imp_govman_stair.nif -Decoding meshes\x\ex_imp_kdoorframe_01.nif -Decoding meshes\x\ex_imp_wall_tower_01.nif -Decoding meshes\x\ex_imp_towerb_top_01.nif -Decoding meshes\x\ex_imp_towers_top_01.nif -Decoding meshes\x\ex_imp_gov_arrowlit.nif -Decoding meshes\x\ex_imp_guardtower_01.nif -Decoding meshes\x\ex_imp_foundation_02.nif -Decoding meshes\x\ex_imp_dragonstatue.nif -Decoding meshes\x\ex_imp_towers_light_01.nif -Decoding meshes\x\ex_imp_towerb_cons_02.nif -Decoding meshes\x\ex_imp_towers_base_01.nif -Decoding meshes\x\ex_imp_towerb_dark_01.nif -Decoding meshes\x\ex_imp_towerb_base_01.nif -Decoding meshes\x\ex_imp_towerb_cons_01.nif -Decoding meshes\x\ex_imp_towers_dark_01.nif -Decoding meshes\x\ex_imp_towerb_light_01.nif -Decoding meshes\x\ex_imp_wall_stairs_02.nif -Decoding meshes\x\ex_imp_wall_stairs_03.nif -Decoding meshes\x\ex_imp_wall_corner_02.nif -Decoding meshes\x\ex_imp_wall_stairs_04.nif -Decoding meshes\x\ex_imp_wall_corner_01.nif -Decoding meshes\x\ex_imp_wall_stairs_01.nif -Decoding meshes\x\ex_imp_govmansion_gate.nif -Decoding meshes\x\ex_imp_govmansion_wing.nif -Decoding meshes\x\ex_imp_towerb_roof_pitch.nif -Decoding meshes\x\ex_imp_govmansion_donjon.nif -Decoding meshes\x\contain_tramaroot_06.nif -Decoding meshes\xyz_pole2.nif -Decoding meshes\x\ex_skiff.nif -Decoding meshes\x\ex_ar_01.nif -Decoding meshes\x\ex_gg_02.nif -Decoding meshes\x\ex_gg_03.nif -Decoding meshes\x\ex_gg_01.nif -Decoding meshes\xbase_anim.nif -Decoding meshes\x\ex_dae_ruin_02_skew.nif -Decoding meshes\x\ex_dae_mehrunesdagon.nif -Decoding meshes\x\ex_dae_b_bo_slpiece.nif -Decoding meshes\x\ex_dae_b_bo_bskirt1.nif -Decoding meshes\x\ex_dae_b_bo_bskirt2.nif -Decoding meshes\x\ex_dae_pillar_02_ruin.nif -Decoding meshes\x\ex_dae_ruin_entry.max.nif -Decoding meshes\x\ex_dae_ruin_04_skew_01.nif -Decoding meshes\x\ex_dae_ruin_04_skew_02.nif -Decoding meshes\x\ex_dae_ruin_02_skew_02.nif -Decoding meshes\x\ex_dae_b_bo_frontskirt.nif -Decoding meshes\x\ex_dae_boethiah_small.nif -Decoding meshes\x\ex_dae_malacath_small.nif -Decoding meshes\x\ex_dae_molagbal_small.nif -Decoding meshes\x\ex_dae_malacath_attack.nif -Decoding meshes\x\ex_dae_malacath_stand.nif -Decoding meshes\x\ex_dae_ruin_platform_01.nif -Decoding meshes\x\ex_dae_buttress_ruin_01.nif -Decoding meshes\x\ex_dae_sheogorath_small.nif -Decoding meshes\x\ex_dae_pillar_02_ruin_02.nif -Decoding meshes\x\ex_com_dframe_lthus.nif -Decoding meshes\x\ex_imp_govmansion_barbican.nif -Decoding meshes\x\ex_de_ship.nif -Decoding meshes\x\ex_de_oar.nif -Decoding meshes\x\ex_trellis.nif -Decoding meshes\x\ex_ruin_10.nif -Decoding meshes\x\ex_ruin_00.nif -Decoding meshes\x\ex_ruin_30.nif -Decoding meshes\x\ex_ruin_20.nif -Decoding meshes\x\ex_ruin_40.nif -Decoding meshes\x\ex_t_hook.nif -Decoding meshes\x\ex_dae_mehrunesdagon_small.nif -Decoding meshes\x\ex_dae_ruin_stair01_short.nif -Decoding meshes\w\w_dai-katana_daedric.nif -Decoding meshes\a\a_templar_m_w_bracer.nif -Decoding meshes\a\a_templar_greaves_k.nif -Decoding meshes\a\a_templar_greaves_g.nif -Decoding meshes\a\a_templar_m_boot_gnd.nif -Decoding meshes\a\a_templar_greaves_ul.nif -Decoding meshes\a\a_templar_pauldron_ua.nif -Decoding meshes\a\a_templar_pauldron_fa.nif -Decoding meshes\a\a_templar_greaves_gnd.nif -Decoding meshes\a\a_templar_m_cl_pauldron.nif -Decoding meshes\a\a_templar_m_cuirass_gnd.nif -Decoding meshes\a\a_masque_clavicus_vile.nif -Decoding meshes\w\w_battleaxe_daedric.nif -Decoding meshes\a\a_trollbone_cuirass_gnd.nif -Decoding meshes\f\furn_imp_rubble_ring.nif -Decoding meshes\f\furn_imp_stoneblock_01.nif -Decoding meshes\w\w_wakazashi_daedric.nif -Decoding meshes\a\a_imperial_greaves_g.nif -Decoding meshes\a\a_imperial_hands.1st.nif -Decoding meshes\a\a_imperial_greaves_k.nif -Decoding meshes\a\a_imperial_m_helmet.nif -Decoding meshes\a\a_imperial_a_boot01.nif -Decoding meshes\a\a_imperial_cl_pauldron.nif -Decoding meshes\a\a_imperial_ua_pauldron.nif -Decoding meshes\a\a_imperial_greaves_gnd.nif -Decoding meshes\a\a_imperial_greaves_ul.nif -Decoding meshes\a\a_imperial_a_boot_gnd.nif -Decoding meshes\a\a_imperial_m_cuirass_gnd.nif -Decoding meshes\a\a_imperial_pauldron_gnd.nif -Decoding meshes\a\a_indoril_m_boot_gnd.nif -Decoding meshes\a\a_indoril_m_fa_mail.nif -Decoding meshes\a\a_indoril_m_hands.1st.nif -Decoding meshes\a\a_indoril_m_cl_pauldron.nif -Decoding meshes\a\a_indoril_m_cuirass_gnd.nif -Decoding meshes\a\a_indoril_m_ua_pauldron.nif -Decoding meshes\a\a_indoril_m_pauldron_gnd.nif -Decoding meshes\a\a_indoril_m_gauntlet_gnd.nif -Decoding meshes\f\terrain_rocks_gl_03.nif -Decoding meshes\f\terrain_rocks_gl_02.nif -Decoding meshes\f\terrain_rocks_gl_01.nif -Decoding meshes\f\terrain_rocks_gl_04.nif -Decoding meshes\f\terrain_cairn_al_01.nif -Decoding meshes\f\terrain_cairn_al_02.nif -Decoding meshes\f\terrain_cairn_al_03.nif -Decoding meshes\f\terrain_rocks_wg_01.nif -Decoding meshes\f\terrain_rocks_wg_03.nif -Decoding meshes\f\terrain_rocks_wg_02.nif -Decoding meshes\f\terrain_rocks_wg_04.nif -Decoding meshes\f\terrain_cairn_ma_01.nif -Decoding meshes\f\terrain_cairn_ma_03.nif -Decoding meshes\f\terrain_cairn_ma_02.nif -Decoding meshes\f\terrain_rocks_ai_01.nif -Decoding meshes\f\terrain_rocks_ai_02.nif -Decoding meshes\f\terrain_rocks_ai_03.nif -Decoding meshes\f\terrain_rocks_ai_04.nif -Decoding meshes\w\w_art_longbow_shade.nif -Decoding meshes\f\furn_uni_weaponrack_01.nif -Decoding meshes\f\furn_uni_spearholder_01.nif -Decoding meshes\w\shadowlongbladeonehand.nif -Decoding meshes\w\shadowlongbladetwoclose.nif -Decoding meshes\f\furn_kneeling_stool_01.nif -Decoding meshes\m\artifact_devourer_01.nif -Decoding meshes\m\artifact_bittercup_01.nif -Decoding meshes\f\terrain_natural_bridge_01.nif -Decoding meshes\a\a_imperial_a_gauntlet_gnd.nif -Decoding meshes\c\c_templar_m_g_skirt.nif -Decoding meshes\l\light_paper_lantern_02.nif -Decoding meshes\l\light_paper_lantern_01.nif -Decoding meshes\l\light_paper_lantern_off.nif -Decoding meshes\f\flora_rm_scathecraw_02.nif -Decoding meshes\f\flora_rm_scathecraw_01.nif -Decoding meshes\b\b_v_wood elf_f_head_01.nif -Decoding meshes\b\b_v_wood elf_m_head_01.nif -Decoding meshes\m\misc_de_fishing_pole.nif -Decoding meshes\f\furn_pole_support_01.nif -Decoding meshes\w\w_art_molagbal_mace.nif -Decoding meshes\w\w_art_mehrunesrazor.nif -Decoding meshes\w\shadowmarksmancrossbow.nif -Decoding meshes\m\misc_redware_platter.nif -Decoding meshes\m\misc_redware_bowl_01.nif -Decoding meshes\m\misc_redware_pitcher.nif -Decoding meshes\f\furn_moldcave_pool00.nif -Decoding meshes\f\furn_moldcave_spout00.nif -Decoding meshes\f\furn_bonecave_pool00.nif -Decoding meshes\f\furn_bonecave_spout00.nif -Decoding meshes\f\furn_com_tapestry_02.nif -Decoding meshes\f\furn_com_cauldron_02.nif -Decoding meshes\f\furn_com_tapestry_03.nif -Decoding meshes\f\furn_com_tapestry_01.nif -Decoding meshes\f\furn_com_wincover_02.nif -Decoding meshes\f\furn_com_wincover_03.nif -Decoding meshes\f\furn_com_wincover_05.nif -Decoding meshes\f\furn_com_wincover_04.nif -Decoding meshes\f\furn_com_tapestry_05.nif -Decoding meshes\f\furn_com_cauldron_01.nif -Decoding meshes\f\furn_com_tapestry_04.nif -Decoding meshes\f\furn_com_bookshelf_01.nif -Decoding meshes\f\furn_com_coatofarms_01.nif -Decoding meshes\f\furn_com_torch_ring_02.nif -Decoding meshes\f\furn_com_coatofarms_02.nif -Decoding meshes\f\furn_com_torch_ring_01.nif -Decoding meshes\f\furn_com_bookshelf_02.nif -Decoding meshes\f\furn_com_lantern_hook.nif -Decoding meshes\f\furn_com_lantern_hook_02.nif -Decoding meshes\l\furn_de_firepit_f_01.nif -Decoding meshes\a\a_newtscale_cuirass.nif -Decoding meshes\b\b_n_imperial_m_knee.nif -Decoding meshes\b\b_n_imperial_f_knee.nif -Decoding meshes\b\b_n_imperial_f_wrist.nif -Decoding meshes\b\b_n_imperial_f_skins.nif -Decoding meshes\b\b_n_imperial_m_skins.nif -Decoding meshes\b\b_n_imperial_m_foot.nif -Decoding meshes\b\b_n_imperial_f_groin.nif -Decoding meshes\b\b_n_imperial_m_groin.nif -Decoding meshes\b\b_n_imperial_m_neck.nif -Decoding meshes\b\b_n_imperial_f_neck.nif -Decoding meshes\b\b_n_imperial_m_wrist.nif -Decoding meshes\b\b_n_imperial_m_ankle.nif -Decoding meshes\b\b_n_imperial_f_ankle.nif -Decoding meshes\b\b_n_imperial_f_foot.nif -Decoding meshes\b\b_n_imperial_m_hair_07.nif -Decoding meshes\b\b_n_imperial_m_hair_05.nif -Decoding meshes\b\b_n_imperial_m_hair_03.nif -Decoding meshes\b\b_n_imperial_m_hair_01.nif -Decoding meshes\b\b_n_imperial_m_hair_09.nif -Decoding meshes\b\b_n_imperial_f_hair_07.nif -Decoding meshes\b\b_n_imperial_f_hair_05.nif -Decoding meshes\b\b_n_imperial_f_hair_03.nif -Decoding meshes\b\b_n_imperial_f_hair_01.nif -Decoding meshes\b\b_n_imperial_m_head_02.nif -Decoding meshes\b\b_n_imperial_m_head_06.nif -Decoding meshes\b\b_n_imperial_m_head_04.nif -Decoding meshes\b\b_n_imperial_f_head_02.nif -Decoding meshes\b\b_n_imperial_f_head_06.nif -Decoding meshes\b\b_n_imperial_f_head_04.nif -Decoding meshes\b\b_n_imperial_m_forearm.nif -Decoding meshes\b\b_n_imperial_m_hair_06.nif -Decoding meshes\b\b_n_imperial_m_hair_04.nif -Decoding meshes\b\b_n_imperial_m_hair_02.nif -Decoding meshes\b\b_n_imperial_m_hair_00.nif -Decoding meshes\b\b_n_imperial_m_hair_08.nif -Decoding meshes\b\b_n_imperial_f_hair_06.nif -Decoding meshes\b\b_n_imperial_f_hair_04.nif -Decoding meshes\b\b_n_imperial_f_hair_02.nif -Decoding meshes\b\b_n_imperial_f_forearm.nif -Decoding meshes\b\b_n_imperial_m_head_03.nif -Decoding meshes\b\b_n_imperial_m_head_01.nif -Decoding meshes\b\b_n_imperial_m_head_07.nif -Decoding meshes\b\b_n_imperial_m_head_05.nif -Decoding meshes\b\b_n_imperial_f_head_03.nif -Decoding meshes\b\b_n_imperial_f_head_01.nif -Decoding meshes\b\b_n_imperial_f_head_07.nif -Decoding meshes\b\b_n_imperial_f_head_05.nif -Decoding meshes\a\a_m_imperialchain_gr_g.nif -Decoding meshes\a\a_m_imperialchain_pa_gnd.nif -Decoding meshes\a\a_m_imperialchain_c_gnd.nif -Decoding meshes\a\a_m_imperialchain_gr_ul.nif -Decoding meshes\a\a_m_imperialchain_helmet.nif -Decoding meshes\a\a_m_imperialchain_gr_gnd.nif -Decoding meshes\a\a_m_imperialchain_pa_ua.nif -Decoding meshes\b\b_n_imperial_f_upper leg.nif -Decoding meshes\b\b_n_imperial_m_upper leg.nif -Decoding meshes\b\b_n_imperial_m_hands.1st.nif -Decoding meshes\b\b_n_imperial_f_upper arm.nif -Decoding meshes\b\b_n_imperial_f_hands.1st.nif -Decoding meshes\b\b_n_imperial_m_upper arm.nif -Decoding meshes\m\pick_grandmaster_01.nif -Decoding meshes\d\door_dwrv_loaddown00.nif -Decoding meshes\f\furn_shrine_rilms_01.nif -Decoding meshes\f\furn_shrine_relms_01.nif -Decoding meshes\f\furn_shrine_felms_01.nif -Decoding meshes\f\furn_shrine_seryn_01.nif -Decoding meshes\f\furn_shrine_delyn_01.nif -Decoding meshes\f\furn_shrine_meris_01.nif -Decoding meshes\f\furn_shrine_roris_01.nif -Decoding meshes\f\furn_shrine_olms_01.nif -Decoding meshes\f\furn_shrine_vivec_01.nif -Decoding meshes\f\furn_shrine_llothis_01.nif -Decoding meshes\f\furn_shrine_aralor_01.nif -Decoding meshes\f\furn_shrine_nerevar_01.nif -Decoding meshes\f\furn_shrine_veloth_01.nif -Decoding meshes\f\furn_shrine_tribunal_01.nif -Decoding meshes\f\act_banner_tel_aruhn.nif -Decoding meshes\f\act_banner_gnaar_mok.nif -Decoding meshes\f\act_banner_tel_mora.nif -Decoding meshes\f\act_banner_ald_velothi.nif -Decoding meshes\f\act_banner_tel_branora.nif -Decoding meshes\f\act_banner_sadrith_mora.nif -Decoding meshes\a\a_m_imperialchain_cuirass.nif -Decoding meshes\l\light_velothismall_01.nif -Decoding meshes\l\light_velothi_brazier.nif -Decoding meshes\a\a_orcish_pauldron_ua.nif -Decoding meshes\a\a_orcish_pauldron_fa.nif -Decoding meshes\a\a_orcish_greaves_gnd.nif -Decoding meshes\a\a_orcish_greaves_ul.nif -Decoding meshes\a\a_orcish_cuirass_gnd.nif -Decoding meshes\a\a_orcish_bracer_gnd.nif -Decoding meshes\a\a_orcish_cl_pauldron.nif -Decoding meshes\a\a_orcish_pauldron_gnd.nif -Decoding meshes\f\flora_trama_shrub_05.nif -Decoding meshes\f\flora_trama_shrub_01.nif -Decoding meshes\f\flora_trama_shrub_06.nif -Decoding meshes\f\flora_trama_shrub_02.nif -Decoding meshes\f\flora_trama_shrub_03.nif -Decoding meshes\f\flora_trama_shrub_04.nif -Decoding meshes\f\flora_treestump_wg_01.nif -Decoding meshes\f\flora_treestump_wg_02.nif -Decoding meshes\a\a_ebony_pauldron_gnd.nif -Decoding meshes\a\a_ebony_cuirass_gnd.nif -Decoding meshes\a\a_ebony_pauldron_fa.nif -Decoding meshes\a\a_ebony_pauldron_ua.nif -Decoding meshes\a\a_ebony_pauldron_cl.nif -Decoding meshes\a\a_ebony_cl_pauldron.nif -Decoding meshes\a\a_ebony_greaves_gnd.nif -Decoding meshes\w\w_art_katana_goldbrand.nif -Decoding meshes\m\app_s_calcinator_01.nif -Decoding meshes\m\app_g_calcinator_01.nif -Decoding meshes\m\app_m_calcinator_01.nif -Decoding meshes\m\app_j_calcinator_01.nif -Decoding meshes\b\b_n_dark elf_f_skins.nif -Decoding meshes\b\b_n_dark elf_m_skins.nif -Decoding meshes\b\b_n_dark elf_m_foot.nif -Decoding meshes\b\b_n_dark elf_f_wrist.nif -Decoding meshes\b\b_n_dark elf_m_neck.nif -Decoding meshes\b\b_n_dark elf_f_neck.nif -Decoding meshes\b\b_n_dark elf_m_knee.nif -Decoding meshes\b\b_n_dark elf_f_knee.nif -Decoding meshes\b\b_n_dark elf_f_groin.nif -Decoding meshes\b\b_n_dark elf_m_groin.nif -Decoding meshes\b\b_n_dark elf_m_wrist.nif -Decoding meshes\b\b_n_dark elf_m_ankle.nif -Decoding meshes\b\b_n_dark elf_f_ankle.nif -Decoding meshes\b\b_n_dark elf_f_foot.nif -Decoding meshes\b\b_n_dark elf_m_hair_23.nif -Decoding meshes\b\b_n_dark elf_m_hair_21.nif -Decoding meshes\b\b_n_dark elf_m_hair_25.nif -Decoding meshes\b\b_n_dark elf_f_hair_23.nif -Decoding meshes\b\b_n_dark elf_f_hair_21.nif -Decoding meshes\b\b_n_dark elf_f_head_02.nif -Decoding meshes\b\b_n_dark elf_f_head_06.nif -Decoding meshes\b\b_n_dark elf_f_head_04.nif -Decoding meshes\b\b_n_dark elf_f_head_08.nif -Decoding meshes\b\b_n_dark elf_m_head_02.nif -Decoding meshes\b\b_n_dark elf_m_head_06.nif -Decoding meshes\b\b_n_dark elf_m_head_04.nif -Decoding meshes\b\b_n_dark elf_m_head_08.nif -Decoding meshes\b\b_n_dark elf_m_head_11.nif -Decoding meshes\b\b_n_dark elf_m_head_13.nif -Decoding meshes\b\b_n_dark elf_m_head_15.nif -Decoding meshes\b\b_n_dark elf_m_head_17.nif -Decoding meshes\b\b_n_dark elf_f_hair_09.nif -Decoding meshes\b\b_n_dark elf_f_hair_05.nif -Decoding meshes\b\b_n_dark elf_f_hair_07.nif -Decoding meshes\b\b_n_dark elf_f_hair_01.nif -Decoding meshes\b\b_n_dark elf_f_hair_03.nif -Decoding meshes\b\b_n_dark elf_m_hair_09.nif -Decoding meshes\b\b_n_dark elf_m_hair_05.nif -Decoding meshes\b\b_n_dark elf_m_hair_07.nif -Decoding meshes\b\b_n_dark elf_m_hair_01.nif -Decoding meshes\b\b_n_dark elf_m_hair_03.nif -Decoding meshes\b\b_n_dark elf_f_hair_16.nif -Decoding meshes\b\b_n_dark elf_f_hair_14.nif -Decoding meshes\b\b_n_dark elf_f_hair_12.nif -Decoding meshes\b\b_n_dark elf_f_hair_10.nif -Decoding meshes\b\b_n_dark elf_f_hair_18.nif -Decoding meshes\b\b_n_dark elf_m_hair_16.nif -Decoding meshes\b\b_n_dark elf_m_hair_14.nif -Decoding meshes\b\b_n_dark elf_m_hair_12.nif -Decoding meshes\b\b_n_dark elf_m_hair_10.nif -Decoding meshes\b\b_n_dark elf_m_hair_18.nif -Decoding meshes\b\b_n_dark elf_m_forearm.nif -Decoding meshes\b\b_n_dark elf_m_hair_22.nif -Decoding meshes\b\b_n_dark elf_m_hair_20.nif -Decoding meshes\b\b_n_dark elf_m_hair_26.nif -Decoding meshes\b\b_n_dark elf_m_hair_24.nif -Decoding meshes\b\b_n_dark elf_f_hair_22.nif -Decoding meshes\b\b_n_dark elf_f_hair_20.nif -Decoding meshes\b\b_n_dark elf_f_hair_24.nif -Decoding meshes\b\b_n_dark elf_f_head_03.nif -Decoding meshes\b\b_n_dark elf_f_head_01.nif -Decoding meshes\b\b_n_dark elf_f_head_07.nif -Decoding meshes\b\b_n_dark elf_f_head_05.nif -Decoding meshes\b\b_n_dark elf_f_head_09.nif -Decoding meshes\b\b_n_dark elf_m_head_03.nif -Decoding meshes\b\b_n_dark elf_m_head_01.nif -Decoding meshes\b\b_n_dark elf_m_head_07.nif -Decoding meshes\b\b_n_dark elf_m_head_05.nif -Decoding meshes\b\b_n_dark elf_m_head_09.nif -Decoding meshes\b\b_n_dark elf_f_head_10.nif -Decoding meshes\b\b_n_dark elf_m_head_10.nif -Decoding meshes\b\b_n_dark elf_m_head_12.nif -Decoding meshes\b\b_n_dark elf_m_head_14.nif -Decoding meshes\b\b_n_dark elf_m_head_16.nif -Decoding meshes\b\b_n_dark elf_f_hair_08.nif -Decoding meshes\b\b_n_dark elf_f_hair_04.nif -Decoding meshes\b\b_n_dark elf_f_hair_06.nif -Decoding meshes\b\b_n_dark elf_f_hair_02.nif -Decoding meshes\b\b_n_dark elf_m_hair_08.nif -Decoding meshes\b\b_n_dark elf_m_hair_04.nif -Decoding meshes\b\b_n_dark elf_m_hair_06.nif -Decoding meshes\b\b_n_dark elf_m_hair_02.nif -Decoding meshes\b\b_n_dark elf_f_forearm.nif -Decoding meshes\b\b_n_dark elf_f_hair_17.nif -Decoding meshes\b\b_n_dark elf_f_hair_15.nif -Decoding meshes\b\b_n_dark elf_f_hair_13.nif -Decoding meshes\b\b_n_dark elf_f_hair_11.nif -Decoding meshes\b\b_n_dark elf_f_hair_19.nif -Decoding meshes\b\b_n_dark elf_m_hair_17.nif -Decoding meshes\b\b_n_dark elf_m_hair_15.nif -Decoding meshes\b\b_n_dark elf_m_hair_13.nif -Decoding meshes\b\b_n_dark elf_m_hair_11.nif -Decoding meshes\b\b_n_dark elf_m_hair_19.nif -Decoding meshes\b\b_n_dark elf_m_upper arm.nif -Decoding meshes\b\b_n_dark elf_f_upper leg.nif -Decoding meshes\b\b_n_dark elf_m_upper leg.nif -Decoding meshes\b\b_n_dark elf_f_hands.1st.nif -Decoding meshes\b\b_n_dark elf_m_hands.1st.nif -Decoding meshes\b\b_n_dark elf_f_upper arm.nif -Decoding meshes\a\a_steel_pauldron_gnd.nif -Decoding meshes\a\a_steel_cuirass_gnd.nif -Decoding meshes\a\a_steel_pauldron_fa.nif -Decoding meshes\a\a_steel_pauldron_ua.nif -Decoding meshes\a\a_steel_pauldron_cl.nif -Decoding meshes\a\a_steel_gauntlet_gnd.nif -Decoding meshes\a\a_steel_greaves_gnd.nif -Decoding meshes\a\a_ringmail_cuirass_gnd.nif -Decoding meshes\b\b_n_nord_m_hands.1st.nif -Decoding meshes\b\b_n_nord_f_upper leg.nif -Decoding meshes\b\b_n_nord_f_hands.1st.nif -Decoding meshes\b\b_n_nord_m_upper leg.nif -Decoding meshes\b\b_n_nord_m_upper arm.nif -Decoding meshes\b\b_n_nord_f_upper arm.nif -Decoding meshes\a\towershield_telvanni.nif -Decoding meshes\a\towershield_bonemold.nif -Decoding meshes\a\towershield_daedric.nif -Decoding meshes\a\towershield_redoranm.nif -Decoding meshes\a\towershield_trollbone.nif -Decoding meshes\a\towershield_dragonscale.nif -Decoding meshes\a\towershield_netch_leather.nif -Decoding meshes\l\light_torch_small_01.nif -Decoding meshes\m\misc_candle_blue_01.nif -Decoding meshes\m\misc_candle_ivory_01.nif -Decoding meshes\m\misc_candle_green_01.nif -Decoding meshes\m\misc_paper_plain_01.nif -Decoding meshes\w\w_shortsword_chitin.nif -Decoding meshes\w\w_shortsword_daedric.nif -Decoding meshes\w\w_shortsword_imperial.nif -Decoding meshes\a\a_boots_heavy_leather.nif -Decoding meshes\w\w_nordic_broadsword.nif -Decoding meshes\b\b_v_redguard_m_head_01.nif -Decoding meshes\b\b_v_redguard_f_head_01.nif -Decoding meshes\c\c_art_ring_vampiric.nif -Decoding meshes\c\c_art_ring_phynaster.nif -Decoding meshes\c\c_art_ring_denstagmer.nif -Decoding meshes\c\c_art_ring_surrounding.nif -Decoding meshes\m\furn_com_coatofarms_01.nif -Decoding meshes\m\probe_secretmaster_01.nif -Decoding meshes\b\b_v_breton_m_head_01.nif -Decoding meshes\b\b_v_breton_f_head_01.nif -Decoding meshes\w\w_broadsword_leafblade.nif -Decoding meshes\w\w_broadsword_imperial.nif -Decoding meshes\f\xfurn_banner_tavern_01.nif -Decoding meshes\f\xfurn_banner_temple_03.nif -Decoding meshes\f\xfurn_banner_temple_01.nif -Decoding meshes\f\xfurn_bannerd_goods_01.nif -Decoding meshes\f\xfurn_banner_hlaalu_01.nif -Decoding meshes\f\xfurn_banner_dagoth_01.nif -Decoding meshes\f\xfurn_banner_temple_02.nif -Decoding meshes\f\xfurn_bannerd_welcome_01.nif -Decoding meshes\f\xfurn_bannerd_wa_shop_01.nif -Decoding meshes\f\xfurn_bannerd_alchemy_01.nif -Decoding meshes\f\xfurn_bannerd_danger_01.nif -Decoding meshes\m\misc_bowl_redware_01.nif -Decoding meshes\m\misc_bowl_redware_03.nif -Decoding meshes\m\misc_bowl_redware_02.nif -Decoding meshes\m\misc_bowl_bugdesign_01.nif -Decoding meshes\m\misc_bowl_glass_peach_01.nif -Decoding meshes\m\misc_com_tankard_01.nif -Decoding meshes\m\misc_com_wood_cup_02.nif -Decoding meshes\m\misc_com_wood_cup_03.nif -Decoding meshes\m\misc_com_wood_knife.nif -Decoding meshes\m\misc_com_wood_cup_01.nif -Decoding meshes\m\misc_com_iron_ladle.nif -Decoding meshes\m\misc_com_wood_cup_04.nif -Decoding meshes\m\misc_com_wood_spoon_01.nif -Decoding meshes\m\misc_com_wood_bowl_03.nif -Decoding meshes\m\misc_com_wood_bowl_01.nif -Decoding meshes\m\misc_com_bucket_metal.nif -Decoding meshes\m\misc_com_wood_spoon_02.nif -Decoding meshes\m\misc_com_wood_bowl_02.nif -Decoding meshes\m\misc_com_wood_bowl_05.nif -Decoding meshes\m\misc_com_wood_bowl_04.nif -Decoding meshes\m\misc_com_metal_plate_04.nif -Decoding meshes\m\misc_com_silverware_fork.nif -Decoding meshes\m\misc_com_metal_goblet_02.nif -Decoding meshes\m\misc_com_metal_goblet_01.nif -Decoding meshes\m\misc_com_metal_plate_03.nif -Decoding meshes\m\misc_com_metal_plate_07.nif -Decoding meshes\m\misc_com_metal_plate_05.nif -Decoding meshes\f\furn_redoran_flag_in.nif -Decoding meshes\f\furn_redoran_flag_01.nif -Decoding meshes\f\furn_redoran_hearth_02.nif -Decoding meshes\f\furn_redoran_shelf_03.nif -Decoding meshes\f\furn_redoran_hearth_01.nif -Decoding meshes\f\furn_redoran_shelf2_01.nif -Decoding meshes\f\furn_redoran_shelf1_01.nif -Decoding meshes\m\misc_mortarpestle_01.nif -Decoding meshes\m\misc_mortarpestle_s_01.nif -Decoding meshes\m\misc_mortarpestle_g_01.nif -Decoding meshes\m\misc_mortarpestle_a_01.nif -Decoding meshes\m\misc_mortarpestle_m_01.nif -Decoding meshes\f\furn_velothi_altar_01.nif -Decoding meshes\m\misc_soulgem_lesser.nif -Decoding meshes\m\misc_soulgem_common.nif -Decoding meshes\m\misc_soulgem_greater.nif -Decoding meshes\m\misc_potion_fresh_01.nif -Decoding meshes\m\misc_potion_cheap_01.nif -Decoding meshes\m\misc_pot_mottled_01.nif -Decoding meshes\m\misc_pot_redware_02.nif -Decoding meshes\m\misc_pot_redware_03.nif -Decoding meshes\m\misc_pot_redware_01.nif -Decoding meshes\m\misc_pot_redware_04.nif -Decoding meshes\m\misc_potion_bargain_01.nif -Decoding meshes\m\misc_potion_quality_01.nif -Decoding meshes\m\misc_potion_exclusive_01.nif -Decoding meshes\m\misc_potion_standard_01.nif -Decoding meshes\m\misc_pot_glass_peach_01.nif -Decoding meshes\m\misc_pot_glass_peach_02.nif -Decoding meshes\f\furn_de_winerack_01.nif -Decoding meshes\f\furn_de_bookshelf_01.nif -Decoding meshes\f\furn_de_practice_mat.nif -Decoding meshes\f\furn_de_signpost_04.nif -Decoding meshes\f\furn_de_signpost_01.nif -Decoding meshes\f\furn_de_signpost_03.nif -Decoding meshes\f\furn_de_signpost_02.nif -Decoding meshes\f\furn_de_tapestry_10.nif -Decoding meshes\f\furn_de_tapestry_01.nif -Decoding meshes\f\furn_de_tapestry_11.nif -Decoding meshes\f\furn_de_tapestry_02.nif -Decoding meshes\f\furn_de_tapestry_12.nif -Decoding meshes\f\furn_de_tapestry_03.nif -Decoding meshes\f\furn_de_tapestry_13.nif -Decoding meshes\f\furn_de_tapestry_04.nif -Decoding meshes\f\furn_de_tapestry_05.nif -Decoding meshes\f\furn_de_tapestry_06.nif -Decoding meshes\f\furn_de_tapestry_07.nif -Decoding meshes\f\furn_de_tapestry_08.nif -Decoding meshes\f\furn_de_tapestry_09.nif -Decoding meshes\f\furn_de_ex_table_02.nif -Decoding meshes\f\furn_de_ex_table_03.nif -Decoding meshes\f\furn_de_bookshelf_02.nif -Decoding meshes\f\furn_de_firepit_f_01.nif -Decoding meshes\f\furn_de_ex_bench_01.nif -Decoding meshes\f\furn_de_ex_stool_02.nif -Decoding meshes\f\furn_de_tapestry_m_01.nif -Decoding meshes\f\furn_de_banner_book_01.nif -Decoding meshes\f\furn_de_banner_book_in.nif -Decoding meshes\f\furn_de_banner_pawn_01.nif -Decoding meshes\f\furn_de_shack_basket_01.nif -Decoding meshes\f\furn_de_shack_basket_02.nif -Decoding meshes\m\misc_com_pitcher_metal_01.nif -Decoding meshes\m\misc_com_silverware_spoon.nif -Decoding meshes\m\misc_com_silverware_knife.nif -Decoding meshes\f\furn_de_banner_telvani_in.nif -Decoding meshes\f\furn_de_banner_telvani_01.nif -Decoding meshes\f\xfurn_bannerd_clothing_01.nif -Decoding meshes\m\misc_bowl_orange_green_01.nif -Decoding meshes\m\misc_bowl_glass_yellow_01.nif -Decoding meshes\o\flora_red_lichen_01.nif -Decoding meshes\o\flora_red_lichen_02.nif -Decoding meshes\o\flora_red_lichen_03.nif -Decoding meshes\m\misc_glass_yellow_01.nif -Decoding meshes\m\misc_glass_green_01.nif -Decoding meshes\r\guar.nif -Decoding meshes\r\xazura.nif -Decoding meshes\r\shalk.nif -Decoding meshes\r\azura.nif -Decoding meshes\r\dreugh.nif -Decoding meshes\r\hunger.nif -Decoding meshes\r\xguar.nif -Decoding meshes\r\xshalk.nif -Decoding meshes\raindrop.nif -Decoding meshes\r\rust rat.nif -Decoding meshes\r\skeleton.nif -Decoding meshes\r\xdagothr.nif -Decoding meshes\r\xdreugh.nif -Decoding meshes\r\xbyagram.nif -Decoding meshes\r\xdremora.nif -Decoding meshes\r\xhunger.nif -Decoding meshes\r\daedroth.nif -Decoding meshes\r\dremora.nif -Decoding meshes\r\dagothr.nif -Decoding meshes\r\bonelord.nif -Decoding meshes\r\byagram.nif -Decoding meshes\r\ashghoul.nif -Decoding meshes\r\ashslave.nif -Decoding meshes\r\nixhound.nif -Decoding meshes\rainsplash.nif -Decoding meshes\r\bonewalker.nif -Decoding meshes\r\lordvivec.nif -Decoding meshes\r\xnixhound.nif -Decoding meshes\r\xminescrib.nif -Decoding meshes\r\xlordvivec.nif -Decoding meshes\r\cliffracer.nif -Decoding meshes\r\clannfear.nif -Decoding meshes\r\minescrib.nif -Decoding meshes\right_arrow.nif -Decoding meshes\r\netch_bull.nif -Decoding meshes\r\xdaedroth.nif -Decoding meshes\r\xduskyalit.nif -Decoding meshes\r\xclannfear.nif -Decoding meshes\r\xbabelfish.nif -Decoding meshes\r\xbonelord.nif -Decoding meshes\r\xashzombie.nif -Decoding meshes\r\xashghoul.nif -Decoding meshes\r\xashslave.nif -Decoding meshes\r\babelfish.nif -Decoding meshes\r\guar_white.nif -Decoding meshes\r\duskyalit.nif -Decoding meshes\r\xskeleton.nif -Decoding meshes\r\ashvampire.nif -Decoding meshes\r\ashzombie.nif -Decoding meshes\r\xrust rat.nif -Decoding meshes\b\b_v_argonian_m_head_01.nif -Decoding meshes\b\b_v_argonian_f_head_01.nif -Decoding meshes\b\b_v_high elf_f_head_01.nif -Decoding meshes\b\b_v_high elf_m_head_01.nif -Decoding meshes\o\flora_stoneflower_02.nif -Decoding meshes\o\flora_stoneflower_01.nif -Decoding meshes\a\a_art_shield_breaker.nif -Decoding meshes\slider_bar.nif -Decoding meshes\smoke_green.nif -Decoding meshes\sky_night_01.nif -Decoding meshes\w\w_art_blade_crescent.nif -Decoding meshes\torchfire.nif -Decoding meshes\w\shadowblunttwoclose.nif -Decoding meshes\c\artifact_bloodring_01.nif -Decoding meshes\c\artifact_belt_hfire_01.nif -Decoding meshes\c\artifact_ring_soul_01.nif -Decoding meshes\c\artifact_amulet_hring_01.nif -Decoding meshes\c\artifact_amulet_hheal_01.nif -Decoding meshes\c\artifact_amulet_hfire_01.nif -Decoding meshes\a\a_art_towershield_eleidon.nif -Decoding meshes\c\artifact_amulet_hthrum_01.nif -Decoding meshes\c\artifact_amulet_htrime_01.nif -Decoding meshes\a\a_glass_pauldron_gnd.nif -Decoding meshes\a\a_glass_cuirass_gnd.nif -Decoding meshes\a\a_glass_pauldron_fa.nif -Decoding meshes\a\a_glass_pauldron_ua.nif -Decoding meshes\a\a_glass_cl_pauldron.nif -Decoding meshes\a\a_glass_greaves_gnd.nif -Decoding meshes\a\a_glass_cl_pauldron_gnd.nif -Decoding meshes\b\b_v_khajiit_m_head_01.nif -Decoding meshes\b\b_v_khajiit_f_head_01.nif -Decoding meshes\f\furn_rail_straight_00.nif -Decoding meshes\w\w_art_claymore_umbra.nif -Decoding meshes\w\w_art_cleaverstfelms.nif -Decoding meshes\w\w_art_crosierstlloth.nif -Decoding meshes\w\w_art_claymore_chrys.nif -Decoding meshes\w\w_art_claymore_iceblade.nif -Decoding meshes\f\furn_lavacave_pool00.nif -Decoding meshes\f\furn_lavacave_spout00.nif -Decoding meshes\f\furn_banner_hlaalu_01.nif -Decoding meshes\f\furn_banner_tavern_in.nif -Decoding meshes\f\furn_banner_temple_01.nif -Decoding meshes\f\furn_bannerd_danger_in.nif -Decoding meshes\f\furn_banner_tavern_01.nif -Decoding meshes\f\furn_banner_temple_04.nif -Decoding meshes\f\furn_bannerd_goods_in.nif -Decoding meshes\f\furn_bannerd_danger_01.nif -Decoding meshes\f\furn_banner_temple_03.nif -Decoding meshes\f\furn_bannerd_goods_01.nif -Decoding meshes\f\furn_banner_dagoth_01.nif -Decoding meshes\f\furn_banner_temple_02.nif -Decoding meshes\f\furn_bannerd_alchemy_01.nif -Decoding meshes\f\furn_bannerd_wa_shop_01.nif -Decoding meshes\f\furn_bannerd_welcome_01.nif -Decoding meshes\f\furn_bannerd_clothing_01.nif -Decoding meshes\f\furn_bannerd_wa_shop_in.nif -Decoding meshes\f\furn_bannerd_alchemy_in.nif -Decoding meshes\f\furn_bannerd_clothing_in.nif -Decoding meshes\f\furn_banner_temple_01_in.nif -Decoding meshes\f\furn_banner_temple_02_in.nif -Decoding meshes\f\furn_banner_temple_03_in.nif -Decoding meshes\f\furn_dae_rubble_01c.nif -Decoding meshes\f\furn_dae_rubble_01b.nif -Decoding meshes\f\furn_dae_rubble_03b.nif -Decoding meshes\f\furn_dae_rubble_01a.nif -Decoding meshes\f\furn_dae_rubble_03a.nif -Decoding meshes\f\furn_dae_rubble_04a.nif -Decoding meshes\f\furn_dae_rubble_pointy.nif -Decoding meshes\upper_arrow.nif -Decoding meshes\a\a_studdedleather_c_gnd.nif -Decoding meshes\a\a_studdedleather_cuirass.nif -Decoding meshes\w\w_dwemer_shortsword.nif -Decoding meshes\m\misc_chest_small_02.nif -Decoding meshes\m\misc_chest_small_01.nif -Decoding meshes\m\text_octavo_open_08.nif -Decoding meshes\m\text_octavo_open_04.nif -Decoding meshes\m\text_octavo_open_06.nif -Decoding meshes\m\text_octavo_open_07.nif -Decoding meshes\m\text_octavo_open_01.nif -Decoding meshes\m\text_octavo_open_02.nif -Decoding meshes\m\text_octavo_open_03.nif -Decoding meshes\f\xex_ashl_e_banner_r.nif -Decoding meshes\f\xex_ashl_a_banner_r.nif -Decoding meshes\f\xex_ashl_z_banner_r.nif -Decoding meshes\f\xex_ashl_u_banner_r.nif -Decoding meshes\m\text_scroll_open_01.nif -Decoding meshes\m\text_scroll_open_02.nif -Decoding meshes\m\text_scroll_open_03.nif -Decoding meshes\a\a_silver_cuirass_duke.nif -Decoding meshes\a\a_daedric_greaves_g.nif -Decoding meshes\a\a_daedric_greaves_ul.nif -Decoding meshes\a\a_daedric_hands.1st.nif -Decoding meshes\a\a_daedric_fountain_h.nif -Decoding meshes\a\a_daedric_boots_gnd.nif -Decoding meshes\a\a_daedric_pauldron_gnd.nif -Decoding meshes\a\a_daedric_pauldron_ua.nif -Decoding meshes\a\a_daedric_greaves_gnd.nif -Decoding meshes\a\a_daedric_pauldron_cl.nif -Decoding meshes\a\a_daedric_cuirass_gnd.nif -Decoding meshes\a\a_daedric_terrifying_h.nif -Decoding meshes\a\a_daedric_gauntlet_gnd.nif -Decoding meshes\f\xfurn_de_banner_book_01.nif -Decoding meshes\f\xfurn_de_banner_pawn_01.nif -Decoding meshes\o\flora_willow_flower_02.nif -Decoding meshes\o\flora_willow_flower_01.nif -Decoding meshes\f\furn_screen_guar_01.nif -Decoding meshes\a\a_art_wraithguard.1st.nif -Decoding meshes\a\a_art_wraithguard_gnd.nif -Decoding meshes\w\w_mace.nif -Decoding meshes\w\w_bolt01.nif -Decoding meshes\w\w_club00.nif -Decoding meshes\w\w_spear.nif -Decoding meshes\w\w_tanto.nif -Decoding meshes\w\w_saber.nif -Decoding meshes\m\misc_vivec_ashmask_01.nif -Decoding meshes\m\misc_silverware_bowl.nif -Decoding meshes\m\misc_silverware_cup.nif -Decoding meshes\m\misc_silverware_cup_01.nif -Decoding meshes\m\misc_silverware_pitcher.nif -Decoding meshes\m\misc_silverware_plate_01.nif -Decoding meshes\m\misc_silverware_plate_03.nif -Decoding meshes\m\misc_silverware_plate_02.nif -Decoding meshes\w\w_staff00.nif -Decoding meshes\w\w_crossbow.nif -Decoding meshes\w\w_longbow.nif -Decoding meshes\w\w_claymore.nif -Decoding meshes\w\w_n_katana.nif -Decoding meshes\w\w_de_fork.nif -Decoding meshes\w\w_arrow01.nif -Decoding meshes\f\xfurn_de_banner_telvani_01.nif -Decoding meshes\d\ex_de_ship_trapdoor.nif -Decoding meshes\d\in_de_shipdoor_toplevel.nif -Decoding meshes\x\ex_t_tower_seedling.nif -Decoding meshes\x\ex_t_tower_strght_lrg.nif -Decoding meshes\x\ex_stronghold_fort00.nif -Decoding meshes\x\ex_strongruin_fort01.nif -Decoding meshes\x\ex_stronghold_door10.nif -Decoding meshes\x\ex_stronghold_wall00.nif -Decoding meshes\x\ex_stronghold_wall02.nif -Decoding meshes\x\ex_stronghold_fort02.nif -Decoding meshes\x\ex_strongruin_fort05.nif -Decoding meshes\x\ex_stronghold_dome00.nif -Decoding meshes\x\ex_strongruin_fort02.nif -Decoding meshes\x\ex_strongruin_fort00.nif -Decoding meshes\x\ex_stronghold_wall03.nif -Decoding meshes\x\ex_stronghold_fort03.nif -Decoding meshes\x\ex_stronghold_fort05.nif -Decoding meshes\x\ex_stronghold_fort01.nif -Decoding meshes\x\ex_stronghold_wall01.nif -Decoding meshes\x\ex_strongruin_dome00.nif -Decoding meshes\x\ex_strongruin_fort03.nif -Decoding meshes\x\ex_stronghold_pylon01.nif -Decoding meshes\x\ex_stronghold_enter00.nif -Decoding meshes\x\ex_stronghold_window00.nif -Decoding meshes\x\ex_stronghold_pylon02.nif -Decoding meshes\x\ex_strong_roofstack00.nif -Decoding meshes\x\ex_stronghold_pylon00.nif -Decoding meshes\x\ex_strongruin_enter00.nif -Decoding meshes\x\ex_t_root_bridge_01.nif -Decoding meshes\x\ex_t_root_spikes_01.nif -Decoding meshes\x\ex_t_root_spikes_02.nif -Decoding meshes\x\ex_t_rock_coastal_03.nif -Decoding meshes\x\ex_t_rock_coastal_01.nif -Decoding meshes\x\ex_t_rock_coastal_02.nif -Decoding meshes\x\ex_t_root_lendsplit.nif -Decoding meshes\x\ex_strongholdruin_wall00.nif -Decoding meshes\x\ex_strongholdruin_wall01.nif -Decoding meshes\x\ex_strongholdruin_wall02.nif -Decoding meshes\x\ex_strongholdruin_wall03.nif -Decoding meshes\x\ex_strongruin_smdwell00.nif -Decoding meshes\x\ex_stronghold_smdwell00.nif -Decoding meshes\x\ex_stronghold_sandpit00.nif -Decoding meshes\x\in_t_housepod_2flr_stair.nif -Decoding meshes\d\in_impsmall_door_01.nif -Decoding meshes\d\in_impsmall_d_cave_01.nif -Decoding meshes\d\in_impsmall_d_hidden_01.nif -Decoding meshes\d\in_impsmall_loaddoor_01.nif -Decoding meshes\d\in_impsmall_door_jail_01.nif -Decoding meshes\d\in_impsmall_door_jail_02.nif -Decoding meshes\x\ex_redoran_tower_01.nif -Decoding meshes\x\ex_redoran_window_01.nif -Decoding meshes\x\ex_redoran_window_02.nif -Decoding meshes\x\ex_redoran_tavern_01.nif -Decoding meshes\x\ex_redoran_steps_01.nif -Decoding meshes\x\ex_redoran_steps_02.nif -Decoding meshes\x\ex_redoran_awning_01.nif -Decoding meshes\x\ex_redoran_barracks_01.nif -Decoding meshes\x\ex_redoran_building_02.nif -Decoding meshes\x\ex_redoran_building_03.nif -Decoding meshes\x\ex_redoran_building_01.nif -Decoding meshes\x\ex_t_doorway_sphere_01.nif -Decoding meshes\x\ex_redoran_building_01a.nif -Decoding meshes\x\ex_velothi_window_01.nif -Decoding meshes\x\ex_velothi_temple_02.nif -Decoding meshes\x\ex_velothi_temple_01.nif -Decoding meshes\x\ex_velothi_tower_01.nif -Decoding meshes\x\ex_velothi_triwin_01.nif -Decoding meshes\x\ex_velothi_hilltent_01.nif -Decoding meshes\x\ex_velothi_entrance_03.nif -Decoding meshes\x\ex_velothi_entrance_01.nif -Decoding meshes\x\ex_velothi_entrance_02.nif -Decoding meshes\x\ex_velothi_tower_01_a.nif -Decoding meshes\x\ex_velothi_striderport_01.nif -Decoding meshes\x\ex_strongruin_fort05_half.nif -Decoding meshes\x\ex_redoran_striderport_01.nif -Decoding meshes\c\amulet_extravagant_1.nif -Decoding meshes\c\amulet_thongofzainab.nif -Decoding meshes\c\amulet_extravagant_2.nif -Decoding meshes\c\amulet_teeth_urshilaku.nif -Decoding meshes\x\ex_common_skywalk_01.nif -Decoding meshes\x\ex_common_window_01.nif -Decoding meshes\x\ex_common_window_02.nif -Decoding meshes\x\ex_common_window_03.nif -Decoding meshes\x\ex_common_plat_cent.nif -Decoding meshes\x\ex_common_plat_rail.nif -Decoding meshes\x\ex_common_plat_corn.nif -Decoding meshes\x\ex_common_tavern_01.nif -Decoding meshes\x\ex_common_lighthouse.nif -Decoding meshes\x\ex_common_balcony_01.nif -Decoding meshes\x\ex_common_dormer_round.nif -Decoding meshes\x\ex_common_house_addon.nif -Decoding meshes\x\ex_common_building_01.nif -Decoding meshes\x\ex_common_building_03.nif -Decoding meshes\x\ex_common_tower_thatch.nif -Decoding meshes\x\ex_common_chimney_tall.nif -Decoding meshes\x\ex_common_entrance_02.nif -Decoding meshes\x\ex_common_building_02.nif -Decoding meshes\x\ex_common_entrance_01.nif -Decoding meshes\x\ex_common_dormer_square.nif -Decoding meshes\x\ex_common_house_tall_01.nif -Decoding meshes\x\ex_common_awning_wood_01.nif -Decoding meshes\x\ex_common_house_tall_02.nif -Decoding meshes\x\ex_common_trellis_withvine.nif -Decoding meshes\x\ex_common_house_mixedroofs.nif -Decoding meshes\x\ex_t_playertower_sprout.nif -Decoding meshes\f\furn_t_fireplace_01.nif -Decoding meshes\f\furn_c_t_dibella_01.nif -Decoding meshes\f\furn_c_t_stendarr_01.nif -Decoding meshes\f\furn_c_t_akatosh_01.nif -Decoding meshes\f\furn_c_t_warrior_01.nif -Decoding meshes\f\furn_c_t_kynareth_01.nif -Decoding meshes\f\furn_c_t_julianos_01.nif -Decoding meshes\f\furn_c_t_zenithar_01.nif -Decoding meshes\f\furn_c_t_apprentice_01.nif -Decoding meshes\f\active_blight_medium.nif -Decoding meshes\f\active_triolith_01a.nif -Decoding meshes\f\active_sign_c_inn_02.nif -Decoding meshes\f\active_com_bar_door.nif -Decoding meshes\f\active_blight_large.nif -Decoding meshes\f\active_blight_small.nif -Decoding meshes\f\active_sign_c_goods_02.nif -Decoding meshes\f\active_sign_c_pwan_01.nif -Decoding meshes\f\active_sign_c_arms_01.nif -Decoding meshes\f\active_sign_c_goods_01.nif -Decoding meshes\f\active_sign_c_arms_02.nif -Decoding meshes\f\active_sign_c_alchemy_01.nif -Decoding meshes\f\active_sign_c_guildm_01.nif -Decoding meshes\f\active_sign_c_guildf_01.nif -Decoding meshes\f\active_sign_c_clothing_01.nif -Decoding meshes\c\c_ring_extravagant_1.nif -Decoding meshes\c\c_ring_extravagant_2.nif -Decoding meshes\x\ex_v_vivecstatue_01.nif -Decoding meshes\x\ex_v_vivecstatue_02.nif -Decoding meshes\x\ex_v_sign_stdeyln_01.nif -Decoding meshes\x\ex_v_sign_stolms_01.nif -Decoding meshes\x\ex_v_sign_hlaalu_01.nif -Decoding meshes\x\ex_v_sign_redoran_01.nif -Decoding meshes\x\ex_v_sign_telvanni_01.nif -Decoding meshes\x\ex_lighthouse_stone.nif -Decoding meshes\x\ex_c_chimney_tall_02.nif -Decoding meshes\x\ex_gg_gateswitch_01.nif -Decoding meshes\x\ex_gg_gatetriolith_01.nif -Decoding meshes\i\in_dwrv_tower_int00.nif -Decoding meshes\w\w_6th_hammer.nif -Decoding meshes\x\ex_cavern_padlock00.nif -Decoding meshes\x\ex_cave_coastrock00.nif -Decoding meshes\x\ex_cave_entrance_10.nif -Decoding meshes\x\ex_vivec_w_slope_01.nif -Decoding meshes\x\ex_vivec_ent_telt_01.nif -Decoding meshes\x\ex_vivec_buttress_01.nif -Decoding meshes\x\ex_vivec_p_water_01.nif -Decoding meshes\x\ex_vivec_b_gap_t_01.nif -Decoding meshes\x\ex_vivec_b_gap_b_01.nif -Decoding meshes\x\ex_vivec_b_gap_b_02.nif -Decoding meshes\x\ex_vivec_b_wb_gap_01.nif -Decoding meshes\x\ex_vivec_wspout_d_02.nif -Decoding meshes\x\ex_vivec_bridgew_01.nif -Decoding meshes\x\ex_vivec_waterfall_03.nif -Decoding meshes\x\ex_vivec_waterfall_05.nif -Decoding meshes\x\ex_vivec_prisonmoon_01.nif -Decoding meshes\x\ex_vivec_waterspout_02.nif -Decoding meshes\x\ex_vivec_waterfall_01.nif -Decoding meshes\x\ex_vivec_bridgewgap_01.nif -Decoding meshes\x\ex_vivec_waterspout_05.nif -Decoding meshes\x\ex_vivec_waterspout_01.nif -Decoding meshes\x\ex_vivec_waterspout_03.nif -Decoding meshes\x\ex_waterfall_mist_01.nif -Decoding meshes\x\ex_waterfall_mist_s_01.nif -Decoding meshes\x\ex_ropebridge_512_01.nif -Decoding meshes\x\ex_ropebridge_1024_01.nif -Decoding meshes\x\ex_ropebridge_2048_01.nif -Decoding meshes\x\ex_ropebridge_stake_01.nif -Decoding meshes\i\in_t_stairs_strt_256.nif -Decoding meshes\i\in_c_stone_room_side.nif -Decoding meshes\i\in_c_stone_room_corner.nif -Decoding meshes\i\in_c_stone_room_center.nif -Decoding meshes\i\in_c_stone_room_entry.nif -Decoding meshes\i\in_c_stone_stair_short.nif -Decoding meshes\i\in_c_stone_hall_small.nif -Decoding meshes\i\in_t_stairs_strt_wiz_256.nif -Decoding meshes\i\in_c_stair_rich_tall_01.nif -Decoding meshes\i\in_c_stair_rich_pend_01.nif -Decoding meshes\i\in_c_stair_plain_tall_01.nif -Decoding meshes\i\in_c_stair_plain_tall_02.nif -Decoding meshes\i\in_c_stair_rich_tall_02.nif -Decoding meshes\i\in_c_stair_rich_pend_02.nif -Decoding meshes\i\in_c_stone_room_c_con_01.nif -Decoding meshes\m\repair_journeyman_01.nif -Decoding meshes\m\repair_secretmaster_01.nif -Decoding meshes\m\repair_grandmaster_01.nif -Decoding meshes\x\ex_t_menhir_crystal.nif -Decoding meshes\i\in_c_stair_thatch_pend_01.nif -Decoding meshes\i\in_c_stair_thatch_pend_02.nif -Decoding meshes\i\in_c_stair_thatch_tall_01.nif -Decoding meshes\i\in_c_stair_thatch_tall_02.nif -Decoding meshes\i\in_c_stairs_rich_ptall_02.nif -Decoding meshes\i\in_c_stairs_rich_ptall_01.nif -Decoding meshes\x\ex_de_docks_centerb.nif -Decoding meshes\x\ex_de_docks_centers.nif -Decoding meshes\x\ex_de_docks_centersb.nif -Decoding meshes\x\ex_de_docks_steps_01.nif -Decoding meshes\x\ex_de_docks_pilings.nif -Decoding meshes\x\ex_de_docks_pilingb.nif -Decoding meshes\x\ex_de_docks_cornerb_01.nif -Decoding meshes\x\ex_de_docks_corners_03.nif -Decoding meshes\x\ex_de_docks_corners_01.nif -Decoding meshes\x\ex_de_docks_corner_02.nif -Decoding meshes\x\ex_de_docks_cornerb_02.nif -Decoding meshes\x\ex_de_docks_corners_02.nif -Decoding meshes\x\ex_de_docks_corner_01.nif -Decoding meshes\x\ex_de_docks_piling_01.nif -Decoding meshes\x\ex_de_docks_cornersb_02.nif -Decoding meshes\x\ex_de_docks_cornersb_01.nif -Decoding meshes\x\ex_de_docks_cornersb_03.nif -Decoding meshes\x\ex_nord_doorrocks_01.nif -Decoding meshes\x\ex_nord_houseshed_01.nif -Decoding meshes\x\ex_daed_wall_512_01.nif -Decoding meshes\x\ex_daed_pillar_claw_01.nif -Decoding meshes\x\ex_bc_cave_entrance.nif -Decoding meshes\x\ex_ac_cave_entrance_01.nif -Decoding meshes\x\ex_bc_cave_entrance_01.nif -Decoding meshes\x\ex_ma_cave_entrance.nif -Decoding meshes\x\ex_ma_cave_entrance_01.nif -Decoding meshes\x\ex_wg_cave_entrance_01.nif -Decoding meshes\x\ex_de_cave_entrance_01.nif -Decoding meshes\x\ex_ai_cave_entrance_01.nif -Decoding meshes\x\ex_rm_cave_entrance_01.nif -Decoding meshes\x\ex_gl_cave_entrance_01.nif -Decoding meshes\x\ex_al_cave_entrance_01.nif -Decoding meshes\x\ex_ma_cave_entrance_lava.nif -Decoding meshes\i\in_ar_shellbottom_01.nif -Decoding meshes\i\in_de_shack_trapdoor.nif -Decoding meshes\i\in_de_shipwreckll_lg.nif -Decoding meshes\i\in_de_shipwreck_top.nif -Decoding meshes\i\in_de_shipwreckul_lg.nif -Decoding meshes\i\in_de_ship_upperlevel.nif -Decoding meshes\i\in_de_ship_lowerlevel.nif -Decoding meshes\i\in_de_shack_trapdoor_01.nif -Decoding meshes\i\in_t_ls_hall_connect.nif -Decoding meshes\i\in_t_ls_hall_connect_01.nif -Decoding meshes\n\potion_local_brew_01.nif -Decoding meshes\n\potion_t_bug_musk_01.nif -Decoding meshes\n\potion_local_liquor_01.nif -Decoding meshes\n\potion_cyro_brandy_01.nif -Decoding meshes\n\potion_cyro_whiskey_01.nif -Decoding meshes\n\potion_comberry_wine_01.nif -Decoding meshes\i\in_impsmall_hall_02.nif -Decoding meshes\i\in_impsmall_hall_03.nif -Decoding meshes\i\in_impsmall_hall_01.nif -Decoding meshes\i\in_impsmall_wall_01.nif -Decoding meshes\i\in_impsmall_3way_01.nif -Decoding meshes\i\in_impsmall_4way_01.nif -Decoding meshes\i\in_impsmall_door_01.nif -Decoding meshes\i\in_impsmall_r_3way_01.nif -Decoding meshes\i\in_impsmall_r_entr_04.nif -Decoding meshes\i\in_impsmall_r_entr_01.nif -Decoding meshes\i\in_impsmall_r_entr_02.nif -Decoding meshes\i\in_impsmall_corner_01.nif -Decoding meshes\i\in_impsmall_shutter_01.nif -Decoding meshes\i\in_impsmall_dj_cave_01.nif -Decoding meshes\i\in_impsmall_endcap_01.nif -Decoding meshes\i\in_impsmall_spiral_01.nif -Decoding meshes\i\in_impsmall_doorjam_01.nif -Decoding meshes\i\in_impsmall_stairs_01.nif -Decoding meshes\i\in_impsmall_r_entr_03.nif -Decoding meshes\i\in_impsmall_r_corner_02.nif -Decoding meshes\i\in_impsmall_trapdoor_01a.nif -Decoding meshes\i\in_impsmall_r_pillar_01.nif -Decoding meshes\i\in_impsmall_dj_hidden_01.nif -Decoding meshes\i\in_impsmall_r_corner_01.nif -Decoding meshes\i\in_impsmall_r_corner_03.nif -Decoding meshes\i\in_impsmall_trapdoor_01.nif -Decoding meshes\i\in_impsmall_r_center_01.nif -Decoding meshes\n\potion_comberry_brandy_01.nif -Decoding meshes\i\in_impsmall_spiral_end_01.nif -Decoding meshes\i\in_impsmall_spiral_bot_01.nif -Decoding meshes\x\ex_lavacave_spout10.nif -Decoding meshes\x\ex_v_palace_steps_01.nif -Decoding meshes\x\ex_v_ban_redoran_01.nif -Decoding meshes\x\ex_v_ban_telvanni_01.nif -Decoding meshes\x\ex_v_ban_stdeyln_01.nif -Decoding meshes\x\ex_v_ban_serving_01.nif -Decoding meshes\x\ex_v_ban_comfort_01.nif -Decoding meshes\x\ex_v_ban_tribunal_01.nif -Decoding meshes\x\ex_hlaalu_bridge_06.nif -Decoding meshes\x\ex_hlaalu_bridge_07.nif -Decoding meshes\x\ex_hlaalu_bridge_04.nif -Decoding meshes\x\ex_hlaalu_bridge_05.nif -Decoding meshes\x\ex_hlaalu_bridge_02.nif -Decoding meshes\x\ex_hlaalu_bridge_03.nif -Decoding meshes\x\ex_hlaalu_bridge_10.nif -Decoding meshes\x\ex_hlaalu_bridge_01.nif -Decoding meshes\x\ex_hlaalu_wall_up_01.nif -Decoding meshes\x\ex_hlaalu_dsteps_01.nif -Decoding meshes\x\ex_hlaalu_dsteps_02.nif -Decoding meshes\x\ex_hlaalu_dsteps_03.nif -Decoding meshes\x\ex_hlaalu_wall_up_02.nif -Decoding meshes\x\ex_hlaalu_balcony_02.nif -Decoding meshes\x\ex_hlaalu_balcony_01.nif -Decoding meshes\x\ex_hlaalu_buttress_04.nif -Decoding meshes\x\ex_hlaalu_wall_end_01.nif -Decoding meshes\x\ex_hlaalu_buttress_05.nif -Decoding meshes\x\ex_hlaalu_wall_gate_03.nif -Decoding meshes\x\ex_hlaalu_wall_gate_01.nif -Decoding meshes\x\ex_hlaalu_buttress_03.nif -Decoding meshes\x\ex_hlaalu_wall_gate_04.nif -Decoding meshes\x\ex_hlaalu_wall_gate_02.nif -Decoding meshes\x\ex_hlaalu_buttress_01.nif -Decoding meshes\x\ex_hlaalu_wall_curve_01.nif -Decoding meshes\x\ex_hlaalu_striderport_01.nif -Decoding meshes\x\ex_holamayan_cover_01.nif -Decoding meshes\i\in_t_council_beams_02.nif -Decoding meshes\i\in_c_connect_stair_short.nif -Decoding meshes\i\in_redoran_ladder_01.nif -Decoding meshes\i\in_redoran_ashpit_02.nif -Decoding meshes\i\in_redoran_s_3way_01.nif -Decoding meshes\i\in_redoran_s_4way_01.nif -Decoding meshes\i\in_redoran_tower_01.nif -Decoding meshes\i\in_redoran_l_join_01.nif -Decoding meshes\i\in_redoran_s_hall_01.nif -Decoding meshes\i\in_redoran_window_01.nif -Decoding meshes\i\in_redoran_tavern_01.nif -Decoding meshes\i\in_redoran_l_hall_01.nif -Decoding meshes\i\in_redoran_steps_02.nif -Decoding meshes\i\in_redoran_steps_03.nif -Decoding meshes\i\in_redoran_l_cap_01.nif -Decoding meshes\i\in_redoran_l_3way_01.nif -Decoding meshes\i\in_redoran_l_4way_01.nif -Decoding meshes\i\in_redoran_s_hall_02.nif -Decoding meshes\i\in_redoran_s_cap_01.nif -Decoding meshes\i\in_redoran_ashpit_01.nif -Decoding meshes\i\in_redoran_barracks_01.nif -Decoding meshes\i\in_redoran_window2_01.nif -Decoding meshes\i\in_redoran_hut_jamb_01.nif -Decoding meshes\i\in_redoran_s_corner_01.nif -Decoding meshes\i\in_redoran_l_corner_01.nif -Decoding meshes\i\in_redoran_woodrail_01.nif -Decoding meshes\i\in_t_doorjamb_hall_small.nif -Decoding meshes\i\in_redoran_staircase_03.nif -Decoding meshes\i\in_redoran_hut_bfloor_02.nif -Decoding meshes\i\in_redoran_hut_bfloor_01.nif -Decoding meshes\i\in_redoran_hut_tfloor_01.nif -Decoding meshes\i\in_redoran_l_doorjamb_01.nif -Decoding meshes\i\in_redoran_staircase_02.nif -Decoding meshes\i\in_redoran_staircase_04.nif -Decoding meshes\i\in_dagoth_scaffold00.nif -Decoding meshes\i\in_t_housepod_stairs.nif -Decoding meshes\i\in_t_housepod_01_hall.nif -Decoding meshes\i\in_t_housepod_pole_01.nif -Decoding meshes\i\in_t_housepod_pole_02.nif -Decoding meshes\i\in_t_housepod_pole_04.nif -Decoding meshes\i\in_t_housepod_pole_03.nif -Decoding meshes\i\in_t_housepod_2ndfloor.nif -Decoding meshes\i\in_t_housepod_2flr_stair.nif -Decoding meshes\i\in_t_housepod_01_hall_02.nif -Decoding meshes\i\in_t_housepod_djamb_exit.nif -Decoding meshes\i\in_velothismall_ws_01.nif -Decoding meshes\i\in_velothilarge_con_01.nif -Decoding meshes\i\in_velothismall_cap_02.nif -Decoding meshes\i\in_velothilarge_cap_01.nif -Decoding meshes\i\in_velothismall_pit_01.nif -Decoding meshes\i\in_velothismall_mid_01.nif -Decoding meshes\i\in_velothi_platform_01.nif -Decoding meshes\i\in_velothismall_dj_01.nif -Decoding meshes\i\in_velothismall_cap_01.nif -Decoding meshes\i\in_velothismall_pit_02.nif -Decoding meshes\i\in_velothismall_pitd_02.nif -Decoding meshes\i\in_velothismall_pitd_04.nif -Decoding meshes\i\in_velothismall_pitd_06.nif -Decoding meshes\i\in_velothi_s_pitstep_01.nif -Decoding meshes\i\in_velothi_s_stairsl_01.nif -Decoding meshes\i\in_velothismall_hall_04.nif -Decoding meshes\i\in_velothismall_rail_01.nif -Decoding meshes\i\in_velothismall_ramp_01.nif -Decoding meshes\i\in_velothismall_ramp_03.nif -Decoding meshes\i\in_velothismall_room_11.nif -Decoding meshes\i\in_velothismall_r8_ramp.nif -Decoding meshes\i\in_velothilarge_ramp_01.nif -Decoding meshes\i\in_velothismall_room_08.nif -Decoding meshes\i\in_velothismall_room_02.nif -Decoding meshes\i\in_velothismall_room_06.nif -Decoding meshes\i\in_velothismall_room_04.nif -Decoding meshes\i\in_velothismall_4way_01.nif -Decoding meshes\i\in_velothismall_3way_01.nif -Decoding meshes\i\in_velothi_s_ceiling_01.nif -Decoding meshes\i\in_velothi_s_ramplong_01.nif -Decoding meshes\i\in_velothi_s_ramplong_02.nif -Decoding meshes\i\in_velothismall_wall_03.nif -Decoding meshes\i\in_velothismall_wall_01.nif -Decoding meshes\i\in_velothi_s_doorjam_01.nif -Decoding meshes\i\in_velothismall_pitd_01.nif -Decoding meshes\i\in_velothismall_pitd_03.nif -Decoding meshes\i\in_velothismall_pitd_05.nif -Decoding meshes\i\in_velothi_s_pitstep_02.nif -Decoding meshes\i\in_velothismall_hall_01.nif -Decoding meshes\i\in_velothismall_hall_03.nif -Decoding meshes\i\in_velothismall_ramp_02.nif -Decoding meshes\i\in_velothilarge_4way_01.nif -Decoding meshes\i\in_velothilarge_3way_01.nif -Decoding meshes\i\in_velothismall_room_10.nif -Decoding meshes\i\in_velothismall_room_12.nif -Decoding meshes\i\in_velothismall_door_01.nif -Decoding meshes\i\in_velothilarge_hall_01.nif -Decoding meshes\i\in_velothilarge_ramp_02.nif -Decoding meshes\i\in_velothismall_room_09.nif -Decoding meshes\i\in_velothismall_room_03.nif -Decoding meshes\i\in_velothismall_room_01.nif -Decoding meshes\i\in_velothismall_room_07.nif -Decoding meshes\i\in_velothismall_room_05.nif -Decoding meshes\i\in_velothismall_3way_02.nif -Decoding meshes\i\in_velothismall_dome_01.nif -Decoding meshes\i\in_velothismall_curve_02.nif -Decoding meshes\i\in_velothismall_curve_01.nif -Decoding meshes\i\in_velothismall_wall_02.nif -Decoding meshes\i\in_velothismall_r8_dome.nif -Decoding meshes\i\in_velothi_s_halfhall_01.nif -Decoding meshes\i\in_stronghold_hall03.nif -Decoding meshes\i\in_stronghold_hall00.nif -Decoding meshes\i\in_stronghold_wall00.nif -Decoding meshes\i\in_stronghold_wall10.nif -Decoding meshes\i\in_stronghold_dome00.nif -Decoding meshes\i\in_stronghold_arch00.nif -Decoding meshes\i\in_stronghold_hall04.nif -Decoding meshes\i\in_strong_balcony10.nif -Decoding meshes\i\in_strong_balcony00.nif -Decoding meshes\i\in_strong_archway00.nif -Decoding meshes\i\in_strong_archway01.nif -Decoding meshes\i\in_strong_shutter00.nif -Decoding meshes\i\in_strongruin_hall02.nif -Decoding meshes\i\in_strong_doorjam00.nif -Decoding meshes\i\in_strong_hallpill00.nif -Decoding meshes\i\in_strongruin_hall01.nif -Decoding meshes\i\in_stronghold_hall02.nif -Decoding meshes\i\in_strong_dualpill00.nif -Decoding meshes\i\in_stronghold_hall01.nif -Decoding meshes\i\in_strongruin_hall00.nif -Decoding meshes\i\in_strongruin_wall10.nif -Decoding meshes\i\in_strongruin_wall00.nif -Decoding meshes\i\in_strongruin2_hall02.nif -Decoding meshes\i\in_stronghold_corr2_05.nif -Decoding meshes\i\in_stronghold_corr2_07.nif -Decoding meshes\i\in_stronghold_corr2_01.nif -Decoding meshes\i\in_stronghold_corr2_03.nif -Decoding meshes\i\in_stronghold_corr3_01.nif -Decoding meshes\i\in_stronghold_stairs00.nif -Decoding meshes\i\in_stronghold_hcorr00.nif -Decoding meshes\i\in_stronghold_hcorr02.nif -Decoding meshes\i\in_stronghold_lpill00.nif -Decoding meshes\i\in_strongruin_corr2_07.nif -Decoding meshes\i\in_strongruin_corr2_05.nif -Decoding meshes\i\in_strongruin_corr2_03.nif -Decoding meshes\i\in_strongruin_corr2_01.nif -Decoding meshes\i\in_strongruin_corr3_01.nif -Decoding meshes\i\in_strongruin_hcorr00.nif -Decoding meshes\i\in_stronghold_corr4_00.nif -Decoding meshes\i\in_stronghold_corr2_04.nif -Decoding meshes\i\in_stronghold_corr2_06.nif -Decoding meshes\i\in_stronghold_corr2_00.nif -Decoding meshes\i\in_stronghold_corr2_02.nif -Decoding meshes\i\in_strongruin2_ramp10.nif -Decoding meshes\i\in_stronghold_corr3_00.nif -Decoding meshes\i\in_strongruin_hcorr01.nif -Decoding meshes\i\in_strongruin_hcorr02.nif -Decoding meshes\i\in_strongruin2_hall01.nif -Decoding meshes\i\in_strongruin2_hall04.nif -Decoding meshes\i\in_strongruin2_hall03.nif -Decoding meshes\i\in_stronghold_hcorr01.nif -Decoding meshes\i\in_strongruin2_hall00.nif -Decoding meshes\i\in_strong_vaultdoor00.nif -Decoding meshes\i\in_strongruin_corr2_06.nif -Decoding meshes\i\in_strongruin_corr2_04.nif -Decoding meshes\i\in_strongruin_corr2_02.nif -Decoding meshes\i\in_strongruin_corr2_00.nif -Decoding meshes\i\in_strongruin_corr3_00.nif -Decoding meshes\i\in_strongruin_stairs00.nif -Decoding meshes\i\in_strongruin2_corr2_04.nif -Decoding meshes\i\in_strongruin2_corr2_02.nif -Decoding meshes\i\in_strongruin2_corr2_00.nif -Decoding meshes\i\in_strongruin2_corr3_02.nif -Decoding meshes\i\in_strongruin2_corr3_00.nif -Decoding meshes\i\in_strongruin2_corr1_00.nif -Decoding meshes\i\in_strongruin2_corr4_00.nif -Decoding meshes\i\in_stronghold_divider10.nif -Decoding meshes\i\in_strongruin_divider10.nif -Decoding meshes\i\in_strongruin2_shutter00.nif -Decoding meshes\i\in_strongruin_roofext00.nif -Decoding meshes\i\in_strongruin_hallpill00.nif -Decoding meshes\i\in_strongruin_collapse10.nif -Decoding meshes\i\in_strongruin2_corr2_03.nif -Decoding meshes\i\in_strongruin2_corr2_01.nif -Decoding meshes\i\in_strongruin2_corr3_01.nif -Decoding meshes\i\in_stronghold_divider00.nif -Decoding meshes\i\in_strong_portal_chamber.nif -Decoding meshes\i\in_stronghold_roofext00.nif -Decoding meshes\i\in_strongruin2_balcony00.nif -Decoding meshes\i\in_strongruin_divider00.nif -Decoding meshes\i\in_strongruin2_balcony10.nif -Decoding meshes\i\in_strongruin2_dualpill00.nif -Decoding meshes\i\in_strongruin2_hallpill00.nif -Decoding meshes\i\in_strongruin2_collapse00.nif -Decoding meshes\i\in_velothismall_pittw_b_01.nif -Decoding meshes\i\in_velothismall_pittd_b_01.nif -Decoding meshes\i\in_velothismall_pitct_b_01.nif -Decoding meshes\i\in_velothismall_pitcb_b_01.nif -Decoding meshes\i\in_velothismall_pitbd_b_01.nif -Decoding meshes\i\in_velothilarge_corner_01.nif -Decoding meshes\i\in_velothismall_pitmw_b_01.nif -Decoding meshes\i\in_velothismall_pittop_02.nif -Decoding meshes\i\in_velothismall_pittop_01.nif -Decoding meshes\i\in_velothismall_fresco_01.nif -Decoding meshes\i\in_velothismall_fresco_02.nif -Decoding meshes\i\in_velothismall_pitc_b_01.nif -Decoding meshes\i\in_velothismall_pitbot_02.nif -Decoding meshes\i\in_velothismall_pitbot_01.nif -Decoding meshes\i\in_velothismall_pitb_b_01.nif -Decoding meshes\i\in_velothismall_column_01.nif -Decoding meshes\i\in_velothismall_column_03.nif -Decoding meshes\i\in_velothismall_column_02.nif -Decoding meshes\i\in_velothilarge_stairs_01.nif -Decoding meshes\i\in_velothismall_pitd_b_02.nif -Decoding meshes\i\in_velothismall_pitd_b_03.nif -Decoding meshes\i\in_velothismall_pitd_b_01.nif -Decoding meshes\i\in_velothismall_corner_01.nif -Decoding meshes\i\in_velothismall_stairs_01.nif -Decoding meshes\i\in_velothismall_pitmid_02.nif -Decoding meshes\i\in_velothismall_pitmid_01.nif -Decoding meshes\i\in_t_monor01_cap_topright.nif -Decoding meshes\d\ex_v_palace_grate_02.nif -Decoding meshes\d\ex_v_palace_grate_01.nif -Decoding meshes\i\in_ashl_tent_banner_08.nif -Decoding meshes\i\in_ashl_tent_banner_04.nif -Decoding meshes\i\in_ashl_tent_banner_06.nif -Decoding meshes\i\in_ashl_tent_banner_02.nif -Decoding meshes\i\in_ashl_tent_banner_16.nif -Decoding meshes\i\in_ashl_tent_banner_14.nif -Decoding meshes\i\in_ashl_tent_banner_12.nif -Decoding meshes\i\in_ashl_tent_banner_10.nif -Decoding meshes\i\in_ashl_tent_banner_09.nif -Decoding meshes\i\in_ashl_tent_banner_05.nif -Decoding meshes\i\in_ashl_tent_banner_07.nif -Decoding meshes\i\in_ashl_tent_banner_01.nif -Decoding meshes\i\in_ashl_tent_banner_03.nif -Decoding meshes\i\in_ashl_tent_banner_15.nif -Decoding meshes\i\in_ashl_tent_banner_13.nif -Decoding meshes\i\in_ashl_tent_banner_11.nif -Decoding meshes\i\in_c_plain_room_side.nif -Decoding meshes\i\in_c_plain_room_center.nif -Decoding meshes\i\in_c_plain_room_corner.nif -Decoding meshes\i\in_c_plain_room_entry.nif -Decoding meshes\i\in_c_plain_hall_small.nif -Decoding meshes\i\in_c_plain_stair_short.nif -Decoding meshes\i\in_c_plain_r_cwin_bay_02.nif -Decoding meshes\i\in_c_plain_r_cwin_bay_01.nif -Decoding meshes\i\in_c_plain_r_swin_bay_01.nif -Decoding meshes\i\in_c_plain_room_cwin_02.nif -Decoding meshes\i\in_c_plain_r_cwin_rec_02.nif -Decoding meshes\i\in_c_plain_r_cwin_rec_03.nif -Decoding meshes\i\in_c_plain_r_cwin_rec_01.nif -Decoding meshes\i\in_c_plain_r_cwin_rec_04.nif -Decoding meshes\i\in_c_plain_r_swin_rec_01.nif -Decoding meshes\i\in_c_plain_r_cwin_tri_01.nif -Decoding meshes\i\in_c_plain_r_cwin_tri_02.nif -Decoding meshes\i\in_c_plain_r_swin_tri_01.nif -Decoding meshes\i\in_c_plain_room_swin_01.nif -Decoding meshes\i\in_c_plain_room_cwin_01.nif -Decoding meshes\d\hlaalu_loaddoor_ 02.nif -Decoding meshes\d\hlaalu_loaddoor_ 01.nif -Decoding meshes\i\in_common_lighthouse.nif -Decoding meshes\i\in_common_room_corner.nif -Decoding meshes\i\in_common_tower_thatch.nif -Decoding meshes\i\in_common_tower_thatch3.nif -Decoding meshes\i\in_common_tower_thatch2.nif -Decoding meshes\a\a_boot_hvy_leather_gnd.nif -Decoding meshes\i\in_c_djamb_rp_arched.nif -Decoding meshes\i\in_c_djamb_rich_arched.nif -Decoding meshes\i\in_c_djamb_plain_arched.nif -Decoding meshes\i\in_c_djamb_stone_square.nif -Decoding meshes\i\in_c_djamb_plain_square.nif -Decoding meshes\i\in_c_djamb_stone_arched.nif -Decoding meshes\menu_scroll_button_up.nif -Decoding meshes\menu_scroll_button_down.nif -Decoding meshes\menu_scroll_button_right.nif -Decoding meshes\menu_scroll_button_left.nif -Decoding meshes\c\c_belt_extravagant_1.nif -Decoding meshes\c\c_belt_extravagant_2.nif -Decoding meshes\i\in_c_thatch_room_pside.nif -Decoding meshes\i\in_c_thatch_room_pcorner.nif -Decoding meshes\i\in_c_thatch_room_pentry.nif -Decoding meshes\i\in_c_thatch_room_pcenter.nif -Decoding meshes\menu_small_energy_bar.nif -Decoding meshes\i\in_c_thatch_room_pcorner2.nif -Decoding meshes\i\in_c_thatch_room_pendside.nif -Decoding meshes\i\in_c_thatch_room_pendside2.nif -Decoding meshes\i\in_c_pillar_wood_tall.nif -Decoding meshes\i\in_c_rich_room_pside.nif -Decoding meshes\i\in_c_rich_room_side.nif -Decoding meshes\i\in_c_rich_room_entry.nif -Decoding meshes\i\in_c_rich_room_center.nif -Decoding meshes\i\in_c_rich_room_pcenter.nif -Decoding meshes\i\in_c_rich_room_pcorner.nif -Decoding meshes\i\in_c_rich_room_pentry.nif -Decoding meshes\i\in_c_rich_room_corner.nif -Decoding meshes\i\in_c_rich_r_swin_bay_01.nif -Decoding meshes\i\in_c_rich_r_cwin_bay_01.nif -Decoding meshes\i\in_c_rich_rp_cwin_bay_04.nif -Decoding meshes\i\in_c_rich_rp_cwin_bay_03.nif -Decoding meshes\i\in_c_rich_rp_cwin_bay_02.nif -Decoding meshes\i\in_c_rich_rp_cwin_bay_01.nif -Decoding meshes\i\in_c_rich_r_cwin_tri_02.nif -Decoding meshes\i\in_c_rich_r_cwin_rec_01.nif -Decoding meshes\i\in_c_rich_r_cwin_rec_03.nif -Decoding meshes\i\in_c_rich_r_swin_rec_01.nif -Decoding meshes\i\in_c_rich_room_pcorner2.nif -Decoding meshes\i\in_c_rich_r_cwin_bay_02.nif -Decoding meshes\i\in_c_rich_room_pendside2.nif -Decoding meshes\i\in_c_rich_r_swin_tri_01.nif -Decoding meshes\i\in_c_rich_r_cwin_tri_01.nif -Decoding meshes\i\in_c_rich_room_pendside.nif -Decoding meshes\i\in_c_rich_r_cwin_rec_02.nif -Decoding meshes\i\in_c_rich_r_cwin_rec_04.nif -Decoding meshes\x\ex_redwall_corner_02.nif -Decoding meshes\x\ex_redwall_corner_01.nif -Decoding meshes\x\ex_redwall_corner_03.nif -Decoding meshes\x\ex_redwall_straight_02.nif -Decoding meshes\x\ex_redwall_straight_01.nif -Decoding meshes\x\ex_dwrv_ruin_tower00.nif -Decoding meshes\x\ex_dwrv_steamstack00.nif -Decoding meshes\x\ex_dwrv_pipefitting00.nif -Decoding meshes\i\in_moldcave_sroom4_05.nif -Decoding meshes\i\in_moldcave_lroom3_02.nif -Decoding meshes\i\in_moldcave_lroom4_02.nif -Decoding meshes\i\in_moldcave_nat_exit00.nif -Decoding meshes\i\in_moldcave_sroom4_01.nif -Decoding meshes\i\in_moldcave_sroom3_01.nif -Decoding meshes\i\in_moldcave_lroom4_03.nif -Decoding meshes\i\in_moldcave_lroom4_01.nif -Decoding meshes\i\in_moldcave_lroom3_01.nif -Decoding meshes\i\in_moldcave_sroom4_04.nif -Decoding meshes\i\in_moldcave_lroom2_00.nif -Decoding meshes\i\in_moldcave_lroom3_00.nif -Decoding meshes\i\in_moldcave_lroom4_05.nif -Decoding meshes\i\in_moldcave_sroom4_03.nif -Decoding meshes\i\in_moldcave_doorway00.nif -Decoding meshes\i\in_moldcave_sroom3_02.nif -Decoding meshes\i\in_moldcave_sroom4_02.nif -Decoding meshes\i\in_moldcave_sroom2_00.nif -Decoding meshes\i\in_moldcave_sroom3_00.nif -Decoding meshes\i\in_moldcave_lroom4_04.nif -Decoding meshes\i\in_nord_ladder_01_a.nif -Decoding meshes\i\in_nord_fireplace_01.nif -Decoding meshes\i\in_nord_rafterboards_01.nif -Decoding meshes\x\ex_t_stair_90_l_short.nif -Decoding meshes\x\ex_t_stair_90_r_short.nif -Decoding meshes\i\in_bonecave_sroom4_05.nif -Decoding meshes\i\in_bonecave_lroom3_02.nif -Decoding meshes\i\in_bonecave_lroom4_02.nif -Decoding meshes\i\in_bonecave_sroom4_01.nif -Decoding meshes\i\in_bonecave_sroom3_01.nif -Decoding meshes\i\in_bonecave_lroom4_03.nif -Decoding meshes\i\in_bonecave_lroom4_01.nif -Decoding meshes\i\in_bonecave_lroom3_01.nif -Decoding meshes\i\in_bonecave_sroom4_04.nif -Decoding meshes\i\in_bonecave_lroom2_00.nif -Decoding meshes\i\in_bonecave_lroom3_00.nif -Decoding meshes\i\in_bonecave_lroom4_05.nif -Decoding meshes\i\in_bonecave_sroom4_03.nif -Decoding meshes\i\in_bonecave_doorway00.nif -Decoding meshes\i\in_bonecave_sroom3_02.nif -Decoding meshes\i\in_bonecave_sroom4_02.nif -Decoding meshes\i\in_bonecave_sroom2_00.nif -Decoding meshes\i\in_bonecave_sroom3_00.nif -Decoding meshes\i\in_bonecave_lroom4_04.nif -Decoding meshes\x\in_c_stair_plain_tall_01.nif -Decoding meshes\x\in_c_stair_plain_tall_02.nif -Decoding meshes\i\in_sewer_collapse00.nif -Decoding meshes\i\in_sewer_anchorlock00.nif -Decoding meshes\i\in_vivec_waterspout_01.nif -Decoding meshes\i\in_cavern_wallmount00.nif -Decoding meshes\x\ex_de_shack_plank_03.nif -Decoding meshes\x\ex_de_shack_plank_04.nif -Decoding meshes\x\ex_de_shack_plank_02.nif -Decoding meshes\x\ex_de_ship_oarright.nif -Decoding meshes\x\ex_de_shack_plank_01.nif -Decoding meshes\x\ex_de_shack_steps_01.nif -Decoding meshes\x\ex_de_shack_awning_01.nif -Decoding meshes\x\ex_de_shack_awning_06.nif -Decoding meshes\x\ex_de_shack_awning_03.nif -Decoding meshes\x\ex_de_shack_awning_04.nif -Decoding meshes\x\ex_de_shack_awning_02.nif -Decoding meshes\x\ex_de_shack_awning_05.nif -Decoding meshes\d\in_redoran_hut_door_01.nif -Decoding meshes\d\in_t_door_small_load.nif -Decoding meshes\d\in_c_door_wood_square.nif -Decoding meshes\d\in_t_housepod_door_exit.nif -Decoding meshes\d\in_t_housepod_djamb_exit.nif -Decoding meshes\d\in_velothismall_ndoor_01.nif -Decoding meshes\x\ex_drystonewall_d_01.nif -Decoding meshes\x\ex_drystonewall_c_01.nif -Decoding meshes\x\ex_drystonewall_s_01.nif -Decoding meshes\x\ex_drystonewall_end_01.nif -Decoding meshes\d\in_strong_vaultdoor00.nif -Decoding meshes\d\ex_redoran_hut_01_a.nif -Decoding meshes\d\ex_t_door_sphere_01.nif -Decoding meshes\d\ex_t_door_slavepod_01.nif -Decoding meshes\d\ex_t_door_stone_large.nif -Decoding meshes\d\ex_redoran_barracks_01_a.nif -Decoding meshes\d\ex_velothi_loaddoor_02.nif -Decoding meshes\d\ex_velothi_loaddoor_01.nif -Decoding meshes\d\ex_velothi_entrance_03_a.nif -Decoding meshes\d\ex_velothi_entrance_01_a.nif -Decoding meshes\x\ex_gnisis_roadmarker_01.nif -Decoding meshes\d\in_redoran_barrackdoor_01.nif -Decoding meshes\i\in_mudcave_sroom4_05.nif -Decoding meshes\i\in_mudcave_lroom3_02.nif -Decoding meshes\i\in_mudcave_lroom4_02.nif -Decoding meshes\i\in_mudcave_sroom4_01.nif -Decoding meshes\i\in_mudcave_sroom3_01.nif -Decoding meshes\i\in_mudcave_lroom4_03.nif -Decoding meshes\i\in_mudcave_lroom4_01.nif -Decoding meshes\i\in_mudcave_lroom3_01.nif -Decoding meshes\i\in_mudcave_sroom4_04.nif -Decoding meshes\i\in_mudcave_lroom2_00.nif -Decoding meshes\i\in_mudcave_lroom3_00.nif -Decoding meshes\i\in_mudcave_lroom4_05.nif -Decoding meshes\i\in_mudcave_sroom4_03.nif -Decoding meshes\i\in_mudcave_doorway00.nif -Decoding meshes\i\in_mudcave_sroom3_02.nif -Decoding meshes\i\in_mudcave_sroom4_02.nif -Decoding meshes\i\in_mudcave_sroom2_00.nif -Decoding meshes\i\in_mudcave_sroom3_00.nif -Decoding meshes\i\in_mudcave_lroom4_04.nif -Decoding meshes\i\in_mudcave_nat_exit00.nif -Decoding meshes\x\ex_terrain_woodstep_01.nif -Decoding meshes\x\ex_t_bridge_lcurved.nif -Decoding meshes\n\ingred_dreugh_wax_01.nif -Decoding meshes\n\ingred_ectoplasm_01.nif -Decoding meshes\n\ingred_fire_salts_01.nif -Decoding meshes\n\ingred_whickwheat_01.nif -Decoding meshes\n\ingred_ash_salts_01.nif -Decoding meshes\n\ingred_hound_meat_01.nif -Decoding meshes\n\ingred_scathecraw_01.nif -Decoding meshes\n\ingred_sload_soap_01.nif -Decoding meshes\n\ingred_bc_spore_pod.nif -Decoding meshes\n\ingred_scamp_skin_01.nif -Decoding meshes\n\ingred_trama_root_01.nif -Decoding meshes\n\ingred_moon_sugar_01.nif -Decoding meshes\n\ingred_guar_hide_01.nif -Decoding meshes\n\ingred_alit_hide_01.nif -Decoding meshes\n\ingred_gold_kanet_01.nif -Decoding meshes\n\ingred_chokeweed_01.nif -Decoding meshes\n\ingred_void_salts_01.nif -Decoding meshes\n\ingred_blonemeal_01.nif -Decoding meshes\n\ingred_fire_petal_01.nif -Decoding meshes\n\ingred_raw_glass_01.nif -Decoding meshes\n\ingred_red_lichen_01.nif -Decoding meshes\n\ingred_gravedust_01.nif -Decoding meshes\n\ingred_hackle-lo_01.nif -Decoding meshes\n\ingred_frost_salts_01.nif -Decoding meshes\n\ingred_ghoul_heart_01.nif -Decoding meshes\n\ingred_green_lichen_01.nif -Decoding meshes\n\ingred_daedra_heart_01.nif -Decoding meshes\n\ingred_daedra_skin_01.nif -Decoding meshes\n\ingred_bc_coda_flower.nif -Decoding meshes\n\ingred_black_anther_01.nif -Decoding meshes\n\ingred_black_lichen_01.nif -Decoding meshes\n\ingred_bc_ampoule_pod.nif -Decoding meshes\n\ingred_bittergreen_01.nif -Decoding meshes\n\ingred_bc_hypha_facia.nif -Decoding meshes\n\ingred_marshmerrow_01.nif -Decoding meshes\n\ingred_kresh_fiber_01.nif -Decoding meshes\n\ingred_kagouti_hide_01.nif -Decoding meshes\n\ingred_kwama_cuttle_01.nif -Decoding meshes\n\ingred_vampire_dust_01.nif -Decoding meshes\n\ingred_racer_plumes_01.nif -Decoding meshes\n\ingred_shalk_resin_01.nif -Decoding meshes\n\ingred_scrib_jelly_01.nif -Decoding meshes\n\ingred_scrib_jerky_01.nif -Decoding meshes\n\ingred_scrap_metal_01.nif -Decoding meshes\n\ingred_stoneflower_01.nif -Decoding meshes\n\ingred_6th_corpusmeat_01.nif -Decoding meshes\n\ingred_6th_corpusmeat_02.nif -Decoding meshes\n\ingred_6th_corpusmeat_03.nif -Decoding meshes\n\ingred_6th_corpusmeat_04.nif -Decoding meshes\n\ingred_6th_corpusmeat_05.nif -Decoding meshes\n\ingred_6th_corpusmeat_06.nif -Decoding meshes\n\ingred_6th_corpusmeat_07.nif -Decoding meshes\n\ingred_willow_anther_01.nif -Decoding meshes\n\ingred_corkbulb_root_01.nif -Decoding meshes\n\ingred_bc_bungler's_bane.nif -Decoding meshes\n\ingred_netch_leather_01.nif -Decoding meshes\d\ex_common_door_balcony.nif -Decoding meshes\n\ingred_corprus_weeping_01.nif -Decoding meshes\x\ex_gg_portcullis_01.nif -Decoding meshes\a\a_iron_gauntlet_gnd.nif -Decoding meshes\a\a_iron_pauldron_gnd.nif -Decoding meshes\d\in_de_llshipdoor_large.nif -Decoding meshes\i\in_hlaalu_room_door3.nif -Decoding meshes\i\in_hlaalu_hall_rail.nif -Decoding meshes\i\in_hlaalu_room_post.nif -Decoding meshes\i\in_hlaalu_room_side.nif -Decoding meshes\i\in_hlaalu_roomt_post.nif -Decoding meshes\i\in_hlaalu_hallt_end.nif -Decoding meshes\i\in_hlaalu_hall_3way.nif -Decoding meshes\i\in_hlaalu_room_rail.nif -Decoding meshes\i\in_hlaalu_hallt_3way.nif -Decoding meshes\i\in_hlaalu_hallt_4way.nif -Decoding meshes\i\in_hlaalu_roomt_side.nif -Decoding meshes\i\in_hlaalu_hall_4way.nif -Decoding meshes\i\in_hlaalu_room_door2.nif -Decoding meshes\i\in_hlaalu_room_entry.nif -Decoding meshes\i\in_hlaalu_room_door4.nif -Decoding meshes\i\in_hlaalu_room_door1.nif -Decoding meshes\i\in_hlaalu_ashpit_02.nif -Decoding meshes\i\in_hlaalu_ashpit_01.nif -Decoding meshes\i\in_hlaalu_hallway_ramp.nif -Decoding meshes\i\in_hlaalu_hallt_3wayd.nif -Decoding meshes\i\in_hlaalu_roomt_entry.nif -Decoding meshes\i\in_hlaalu_platform_02.nif -Decoding meshes\i\in_hlaalu_roomt_sided.nif -Decoding meshes\i\in_hlaalu_loaddoor_01.nif -Decoding meshes\i\in_hlaalu_hall_stairsr.nif -Decoding meshes\i\in_hlaalu_hall_stairsl.nif -Decoding meshes\i\in_hlaalu_loaddoor_02.nif -Decoding meshes\i\in_hlaalu_room_center.nif -Decoding meshes\i\in_hlaalu_hallway_end.nif -Decoding meshes\i\in_hlaalu_hallt_center.nif -Decoding meshes\i\in_hlaalu_room_stairsl.nif -Decoding meshes\i\in_hlaalu_room_stairsr.nif -Decoding meshes\i\in_hlaalu_platform_01.nif -Decoding meshes\i\in_hlaalu_hallt_entry.nif -Decoding meshes\i\in_hlaalu_room_corner.nif -Decoding meshes\i\in_hlaalu_roomt_center.nif -Decoding meshes\i\in_hlaalu_roomt_cornd_02.nif -Decoding meshes\i\in_hlaalu_roomt_cornd_01.nif -Decoding meshes\i\in_hlaalu_room_center_01.nif -Decoding meshes\i\in_hlaalu_hallway_stairs.nif -Decoding meshes\i\in_hlaalu_hallt_centerd.nif -Decoding meshes\i\in_hlaalu_hallway_center.nif -Decoding meshes\i\in_hlaalu_hall_corner_01.nif -Decoding meshes\i\in_hlaalu_hallt_cornd_02.nif -Decoding meshes\i\in_hlaalu_hallt_cornd_01.nif -Decoding meshes\i\in_hlaalu_doorjamb_load.nif -Decoding meshes\i\in_pycave_lroom3_01.nif -Decoding meshes\i\in_pycave_sroom3_01.nif -Decoding meshes\i\in_pycave_lroom3_00.nif -Decoding meshes\i\in_pycave_sroom3_00.nif -Decoding meshes\i\in_pycave_lroom3_02.nif -Decoding meshes\i\in_pycave_sroom3_02.nif -Decoding meshes\i\in_pycave_doorway00.nif -Decoding meshes\i\in_pycave_lroom2_00.nif -Decoding meshes\i\in_pycave_sroom2_00.nif -Decoding meshes\i\in_pycave_nat_exit00.nif -Decoding meshes\i\in_pycave_sroom4_04.nif -Decoding meshes\i\in_pycave_lroom4_04.nif -Decoding meshes\i\in_pycave_sroom4_05.nif -Decoding meshes\i\in_pycave_lroom4_05.nif -Decoding meshes\i\in_pycave_sroom4_01.nif -Decoding meshes\i\in_pycave_lroom4_01.nif -Decoding meshes\i\in_pycave_sroom4_02.nif -Decoding meshes\i\in_pycave_lroom4_02.nif -Decoding meshes\i\in_pycave_sroom4_03.nif -Decoding meshes\i\in_pycave_lroom4_03.nif -Decoding meshes\i\in_t_cave_conect_wiz.nif -Decoding meshes\i\in_t_cave_conect_plain.nif -Decoding meshes\i\in_t_cave_conect_endcap.nif -Decoding meshes\a\shield_netch_leather.nif -Decoding meshes\a\shield_nordic_leather.nif -Decoding meshes\i\in_t_manor01_leftcap.nif -Decoding meshes\i\in_t_manor02_backcap.nif -Decoding meshes\i\in_t_manor01_backcap.nif -Decoding meshes\i\in_t_manor_floor_01.nif -Decoding meshes\i\in_t_manor_stairs_01.nif -Decoding meshes\i\in_t_manor02_leftcap.nif -Decoding meshes\i\in_t_manor01_rightcap.nif -Decoding meshes\i\in_t_manor02_cap_right.nif -Decoding meshes\i\in_t_manor01_frontcap.nif -Decoding meshes\i\in_t_manor01_entry_top.nif -Decoding meshes\i\in_t_manor02_rightcap.nif -Decoding meshes\i\in_t_manor02_floor_01.nif -Decoding meshes\i\in_t_manor01_floor_01.nif -Decoding meshes\i\in_t_manor_djamb_exit.nif -Decoding meshes\i\in_t_manor01_cap_left.nif -Decoding meshes\i\in_t_manor02_cap_left.nif -Decoding meshes\i\in_t_manor02_entry_right.nif -Decoding meshes\i\in_t_manor01_entry_left.nif -Decoding meshes\i\in_t_manor02_entry_left.nif -Decoding meshes\i\in_t_railing_pole_rails.nif -Decoding meshes\i\in_lava_blacksquare.nif -Decoding meshes\i\in_lavacave_sroom4_05.nif -Decoding meshes\i\in_lavacave_lroom3_02.nif -Decoding meshes\i\in_lavacave_lroom4_02.nif -Decoding meshes\i\in_lavacave_nat_exit00.nif -Decoding meshes\i\in_lavacave_sroom4_01.nif -Decoding meshes\i\in_lavacave_sroom3_01.nif -Decoding meshes\i\in_lavacave_lroom4_03.nif -Decoding meshes\i\in_lavacave_lroom4_01.nif -Decoding meshes\i\in_lavacave_lroom3_01.nif -Decoding meshes\i\in_lavacave_sroom4_04.nif -Decoding meshes\i\in_lavacave_lroom2_00.nif -Decoding meshes\i\in_lavacave_lroom3_00.nif -Decoding meshes\i\in_lavacave_lroom4_05.nif -Decoding meshes\i\in_lavacave_sroom4_03.nif -Decoding meshes\i\in_lavacave_doorway00.nif -Decoding meshes\i\in_lavacave_sroom3_02.nif -Decoding meshes\i\in_lavacave_sroom4_02.nif -Decoding meshes\i\in_lavacave_sroom2_00.nif -Decoding meshes\i\in_lavacave_sroom3_00.nif -Decoding meshes\i\in_lavacave_lroom4_04.nif -Decoding meshes\i\in_t_manor02_entry_right01.nif -Decoding meshes\i\in_t_manor01_cap_topright.nif -Decoding meshes\i\in_hlaalu_roomt_corner_01.nif -Decoding meshes\i\in_hlaalu_roomt_corner_02.nif -Decoding meshes\i\in_hlaalu_roomt_corner_03.nif -Decoding meshes\i\in_hlaalu_hallt_corner_01.nif -Decoding meshes\r\xg_centurionspider.nif -Decoding meshes\x\ex_gg_fence_s_h_01.nif -Decoding meshes\x\ex_gg_particles_01.nif -Decoding meshes\x\ex_de_docks_3wayb.nif -Decoding meshes\x\ex_de_docks_4wayb.nif -Decoding meshes\x\ex_de_docks_3ways.nif -Decoding meshes\x\ex_de_shack_steps.nif -Decoding meshes\x\ex_de_docks_cleat.nif -Decoding meshes\x\ex_de_docks_center.nif -Decoding meshes\x\ex_de_ship_oarleft.nif -Decoding meshes\x\ex_de_scaffold_03.nif -Decoding meshes\x\ex_de_scaffold_02.nif -Decoding meshes\x\ex_de_scaffold_01.nif -Decoding meshes\x\ex_de_docks_3waysb.nif -Decoding meshes\x\ex_de_docks_steps.nif -Decoding meshes\x\ex_de_docks_piling.nif -Decoding meshes\x\ex_cave_stoneyb00.nif -Decoding meshes\x\ex_vivec_sbase_01.nif -Decoding meshes\x\ex_vivec_bridge_06.nif -Decoding meshes\x\ex_vivec_bridge_07.nif -Decoding meshes\x\ex_vivec_bridge_04.nif -Decoding meshes\x\ex_vivec_bridge_02.nif -Decoding meshes\x\ex_vivec_bridge_01.nif -Decoding meshes\x\ex_vivec_bt_tb_01.nif -Decoding meshes\x\ex_vivec_palace_01.nif -Decoding meshes\x\ex_vivec_ent_t_01.nif -Decoding meshes\x\ex_vivec_ent_t_02.nif -Decoding meshes\x\ex_vivec_ent_b_01.nif -Decoding meshes\x\ex_t_stair_spiral.nif -Decoding meshes\x\ex_t_stair_rcurve.nif -Decoding meshes\x\ex_t_stair_lcurve.nif -Decoding meshes\x\ex_v_sign_hallj_01.nif -Decoding meshes\x\ex_v_sign_arena_01.nif -Decoding meshes\x\ex_v_sign_hallw_01.nif -Decoding meshes\x\ex_dwrv_boulder20.nif -Decoding meshes\x\ex_dwrv_boulder30.nif -Decoding meshes\x\ex_dwrv_boulder00.nif -Decoding meshes\x\ex_dwrv_boulder10.nif -Decoding meshes\x\ex_nord_chimney_01.nif -Decoding meshes\x\in_c_doorframe_01.nif -Decoding meshes\x\ex_t_root_arch_01.nif -Decoding meshes\x\ex_imp_towerb_top.nif -Decoding meshes\x\ex_imp_statue_base.nif -Decoding meshes\x\ex_imp_mooring_01.nif -Decoding meshes\x\ex_imp_entrance_01.nif -Decoding meshes\x\ex_imp_wallent_01.nif -Decoding meshes\x\ex_imp_wallent_02.nif -Decoding meshes\x\ex_t_gateway_great.nif -Decoding meshes\x\ex_v_foundation_01.nif -Decoding meshes\x\ex_v_foundation_02.nif -Decoding meshes\x\ex_v_foundation_03.nif -Decoding meshes\x\ex_dae_b_bo_cape2.nif -Decoding meshes\x\ex_dae_b_bo_cape3.nif -Decoding meshes\x\ex_dae_b_bo_cape1.nif -Decoding meshes\x\ex_dae_wall_256_09.nif -Decoding meshes\x\ex_dae_wall_256_08.nif -Decoding meshes\x\ex_dae_wall_256_01.nif -Decoding meshes\x\ex_dae_wall_256_10.nif -Decoding meshes\x\ex_dae_wall_256_03.nif -Decoding meshes\x\ex_dae_wall_256_13.nif -Decoding meshes\x\ex_dae_wall_256_02.nif -Decoding meshes\x\ex_dae_wall_256_05.nif -Decoding meshes\x\ex_dae_wall_256_04.nif -Decoding meshes\x\ex_dae_wall_256_07.nif -Decoding meshes\x\ex_dae_wall_256_06.nif -Decoding meshes\x\ex_dae_wall_512_09.nif -Decoding meshes\x\ex_dae_wall_512_08.nif -Decoding meshes\x\ex_dae_wall_512_05.nif -Decoding meshes\x\ex_dae_wall_512_04.nif -Decoding meshes\x\ex_dae_wall_512_07.nif -Decoding meshes\x\ex_dae_wall_512_06.nif -Decoding meshes\x\ex_dae_wall_512_01.nif -Decoding meshes\x\ex_dae_wall_512_10.nif -Decoding meshes\x\ex_dae_wall_512_03.nif -Decoding meshes\x\ex_dae_wall_512_02.nif -Decoding meshes\x\ex_dae_azura_small.nif -Decoding meshes\x\ex_dae_sheogorath.nif -Decoding meshes\x\ex_dae_b_bo_ubody.nif -Decoding meshes\x\ex_dae_b_bo_lbody.nif -Decoding meshes\x\ex_redoran_hut_02.nif -Decoding meshes\x\ex_redoran_hut_01.nif -Decoding meshes\x\ex_redwall_post_01.nif -Decoding meshes\x\ex_redwall_arch_01.nif -Decoding meshes\x\ex_t_clawgrowth_02.nif -Decoding meshes\x\ex_t_clawgrowth_01.nif -Decoding meshes\x\ex_v_ban_vivec_01.nif -Decoding meshes\x\ex_v_ban_vivec_02.nif -Decoding meshes\x\ex_v_ban_arena_01.nif -Decoding meshes\x\ex_v_ban_faith_01.nif -Decoding meshes\x\ex_v_ban_stolms_01.nif -Decoding meshes\x\ex_v_ban_count_01.nif -Decoding meshes\x\ex_v_ban_hlaalu_01.nif -Decoding meshes\x\ex_v_ban_child_01.nif -Decoding meshes\x\ex_v_ban_speak_01.nif -Decoding meshes\x\ex_scave2_enter10.nif -Decoding meshes\x\ex_hlaalu_pole_01.nif -Decoding meshes\x\ex_hlaalu_pole_02.nif -Decoding meshes\x\ex_hlaalu_wall_01.nif -Decoding meshes\x\ex_hlaalu_wall_02.nif -Decoding meshes\x\ex_hlaalu_canal_05.nif -Decoding meshes\x\ex_hlaalu_canal_04.nif -Decoding meshes\x\ex_hlaalu_canal_07.nif -Decoding meshes\x\ex_hlaalu_canal_06.nif -Decoding meshes\x\ex_hlaalu_canal_11.nif -Decoding meshes\x\ex_hlaalu_canal_01.nif -Decoding meshes\x\ex_hlaalu_canal_10.nif -Decoding meshes\x\ex_hlaalu_canal_13.nif -Decoding meshes\x\ex_hlaalu_canal_03.nif -Decoding meshes\x\ex_hlaalu_canal_12.nif -Decoding meshes\x\ex_hlaalu_canal_02.nif -Decoding meshes\x\ex_hlaalu_canal_09.nif -Decoding meshes\x\ex_hlaalu_canal_08.nif -Decoding meshes\x\ex_hlaalu_steps_02.nif -Decoding meshes\x\ex_hlaalu_steps_03.nif -Decoding meshes\x\ex_hlaalu_steps_01.nif -Decoding meshes\x\ex_hlaalu_steps_06.nif -Decoding meshes\x\ex_hlaalu_steps_07.nif -Decoding meshes\x\ex_hlaalu_steps_04.nif -Decoding meshes\x\ex_hlaalu_steps_05.nif -Decoding meshes\x\ex_common_trellis.nif -Decoding meshes\x\ex_common_plat_lrg.nif -Decoding meshes\x\ex_common_plat_end.nif -Decoding meshes\x\ex_velothi_dome_01.nif -Decoding meshes\x\ex_siltstrider_01.nif -Decoding meshes\x\ex_siltstrider_02.nif -Decoding meshes\x\ex_siltstrider_03.nif -Decoding meshes\x\ex_siltstrider_04.nif -Decoding meshes\x\ex_siltstrider_05.nif -Decoding meshes\x\ex_ruin_boulder01.nif -Decoding meshes\x\ex_ruin_boulder00.nif -Decoding meshes\x\ex_ruin_boulder03.nif -Decoding meshes\x\ex_ruin_boulder02.nif -Decoding meshes\x\ex_ruin_boulder05.nif -Decoding meshes\x\ex_ruin_boulder04.nif -Decoding meshes\x\ex_ashl_banner_01.nif -Decoding meshes\x\ex_t_housestem_02.nif -Decoding meshes\x\ex_t_housestem_03.nif -Decoding meshes\x\ex_t_housestem_01.nif -Decoding meshes\x\ex_ashl_door_01.nif -Decoding meshes\x\ex_ashl_tent_03.nif -Decoding meshes\x\ex_ashl_tent_01.nif -Decoding meshes\x\ex_ashl_door_02.nif -Decoding meshes\x\ex_ashl_tent_04.nif -Decoding meshes\x\ex_ashl_tent_02.nif -Decoding meshes\x\ex_trellis_vine.nif -Decoding meshes\x\ex_dwrv_walker20.nif -Decoding meshes\x\ex_dwrv_walker10.nif -Decoding meshes\x\ex_dwrv_walker00.nif -Decoding meshes\x\ex_dwrv_observ00.nif -Decoding meshes\x\ex_dwrv_block10.nif -Decoding meshes\x\ex_dwrv_block20.nif -Decoding meshes\x\ex_dwrv_header00.nif -Decoding meshes\x\ex_dwrv_statue00.nif -Decoding meshes\x\ex_dwrv_enter00.nif -Decoding meshes\x\ex_dwrv_bridge10.nif -Decoding meshes\x\ex_dwrv_bridge00.nif -Decoding meshes\x\ex_dwrv_block00.nif -Decoding meshes\x\ex_dwrv_alcove10.nif -Decoding meshes\x\ex_dwrv_cosmo00.nif -Decoding meshes\x\ex_dwrv_block30.nif -Decoding meshes\x\ex_dwrv_alcove00.nif -Decoding meshes\x\ex_ruin_rubble20.nif -Decoding meshes\x\ex_ruin_rubble00.nif -Decoding meshes\x\ex_ruin_rubble10.nif -Decoding meshes\x\ex_dae_pillar_02.nif -Decoding meshes\x\ex_dae_pillar_03.nif -Decoding meshes\x\ex_dae_pillar_01.nif -Decoding meshes\x\ex_dae_b_bo_head.nif -Decoding meshes\x\ex_dae_boethiah.nif -Decoding meshes\x\ex_daed_ruin_01.nif -Decoding meshes\x\ex_dae_b_bo_axe.nif -Decoding meshes\x\ex_dae_molagbal.nif -Decoding meshes\x\ex_barnacles_01.nif -Decoding meshes\x\ex_barnacles_03.nif -Decoding meshes\x\ex_barnacles_05.nif -Decoding meshes\x\ex_barnacles_02.nif -Decoding meshes\x\ex_barnacles_04.nif -Decoding meshes\x\ex_barnacles_06.nif -Decoding meshes\x\ex_cave_grass00.nif -Decoding meshes\x\ex_cave_scrub00.nif -Decoding meshes\x\ex_gg_fence_s_04.nif -Decoding meshes\x\ex_gg_fence_s_03.nif -Decoding meshes\x\ex_gg_fence_s_02.nif -Decoding meshes\x\ex_gg_fence_s_01.nif -Decoding meshes\x\ex_redwall_up_01.nif -Decoding meshes\x\ex_redwall_b_01.nif -Decoding meshes\x\ex_redwall_b_03.nif -Decoding meshes\x\ex_redwall_b_05.nif -Decoding meshes\x\ex_redwall_p_01.nif -Decoding meshes\x\ex_redwall_p_03.nif -Decoding meshes\x\ex_redwall_p_05.nif -Decoding meshes\x\ex_redwall_b_02.nif -Decoding meshes\x\ex_redwall_b_04.nif -Decoding meshes\x\ex_redwall_b_06.nif -Decoding meshes\x\ex_redwall_p_02.nif -Decoding meshes\x\ex_redwall_p_04.nif -Decoding meshes\x\ex_de_constr_03.nif -Decoding meshes\x\ex_de_constr_01.nif -Decoding meshes\x\ex_de_constr_05.nif -Decoding meshes\x\ex_de_railing_01.nif -Decoding meshes\x\ex_de_railing_03.nif -Decoding meshes\x\ex_de_railing_02.nif -Decoding meshes\x\ex_de_railing_05.nif -Decoding meshes\x\ex_de_railing_04.nif -Decoding meshes\x\ex_de_railing_06.nif -Decoding meshes\x\ex_de_docks_128.nif -Decoding meshes\x\ex_de_shipwreck.nif -Decoding meshes\x\ex_de_docks_3way.nif -Decoding meshes\x\ex_de_docks_4way.nif -Decoding meshes\x\ex_de_docks_endc.nif -Decoding meshes\x\ex_de_docks_ends.nif -Decoding meshes\x\ex_de_constr_02.nif -Decoding meshes\x\ex_de_constr_06.nif -Decoding meshes\x\ex_de_constr_04.nif -Decoding meshes\x\ex_de_docks_end.nif -Decoding meshes\x\ex_de_docks_gate.nif -Decoding meshes\x\ex_de_shack_door.nif -Decoding meshes\x\ex_vivec_hfq_03.nif -Decoding meshes\x\ex_vivec_hfq_01.nif -Decoding meshes\x\ex_vivec_pqs_02.nif -Decoding meshes\x\ex_vivec_b_tb_01.nif -Decoding meshes\x\ex_vivec_b_wb_01.nif -Decoding meshes\x\ex_vivec_g_r_01.nif -Decoding meshes\x\ex_vivec_hfq_04.nif -Decoding meshes\x\ex_vivec_hfq_02.nif -Decoding meshes\x\ex_vivec_pqs_01.nif -Decoding meshes\x\ex_vivec_w_c_01.nif -Decoding meshes\x\ex_vivec_w_g_01.nif -Decoding meshes\x\ex_vivec_w_e_01.nif -Decoding meshes\x\ex_vivec_telt_01.nif -Decoding meshes\x\ex_vivec_g_r_02.nif -Decoding meshes\x\ex_vivec_b_t_01.nif -Decoding meshes\x\ex_ci_doorjam_01.nif -Decoding meshes\x\ex_volcano_steam.nif -Decoding meshes\x\ex_ropebridge_01.nif -Decoding meshes\x\ex_nord_rock_01.nif -Decoding meshes\x\ex_nord_well_01.nif -Decoding meshes\x\ex_nord_door_02.nif -Decoding meshes\x\ex_nord_rock_02.nif -Decoding meshes\x\ex_nord_house_03.nif -Decoding meshes\x\ex_nord_house_02.nif -Decoding meshes\x\ex_nord_house_01.nif -Decoding meshes\x\ex_nord_house_05.nif -Decoding meshes\x\ex_nord_house_04.nif -Decoding meshes\x\ex_nord_doorf_01.nif -Decoding meshes\x\ex_holamayan_01.nif -Decoding meshes\x\ex_emp_tower_01.nif -Decoding meshes\x\ex_imp_arrowslit.nif -Decoding meshes\x\ex_imp_bridge_01.nif -Decoding meshes\x\ex_imp_bridge_02.nif -Decoding meshes\x\ex_hlaalu_fb_01.nif -Decoding meshes\x\ex_hlaalu_win_03.nif -Decoding meshes\x\ex_hlaalu_win_02.nif -Decoding meshes\x\ex_hlaalu_win_01.nif -Decoding meshes\x\ex_hlaalu_bal_02.nif -Decoding meshes\x\ex_hlaalu_bal_01.nif -Decoding meshes\x\ex_hlaalu_fb_02.nif -Decoding meshes\x\ex_t_housepod_01.nif -Decoding meshes\x\ex_t_housepod_03.nif -Decoding meshes\x\ex_t_housepod_02.nif -Decoding meshes\x\ex_t_housepod_04.nif -Decoding meshes\x\ex_t_spiralramp.nif -Decoding meshes\x\ex_t_councilhall.nif -Decoding meshes\x\ex_t_pole_hooked.nif -Decoding meshes\x\ex_t_tower_bent.nif -Decoding meshes\x\ex_t_menhir_l_01.nif -Decoding meshes\x\ex_t_doorway_01.nif -Decoding meshes\x\ex_t_root_xl_03.nif -Decoding meshes\x\ex_t_root_xl_01.nif -Decoding meshes\x\ex_t_bigroot_01.nif -Decoding meshes\x\ex_t_slavemarket.nif -Decoding meshes\x\ex_t_slavepod_01.nif -Decoding meshes\x\ex_t_platform_01.nif -Decoding meshes\x\ex_t_platform_02.nif -Decoding meshes\x\ex_t_doorway_02.nif -Decoding meshes\x\ex_t_root_xl_02.nif -Decoding meshes\x\ex_t_bigroot_02.nif -Decoding meshes\x\ex_v_ban_walk_01.nif -Decoding meshes\x\ex_v_2_floor_01.nif -Decoding meshes\x\ex_v_sign_fq_01.nif -Decoding meshes\x\ex_v_1_corner_01.nif -Decoding meshes\x\ex_v_2_corner_01.nif -Decoding meshes\x\ex_v_ban_imp_01.nif -Decoding meshes\x\ex_v_stdeyln_01.nif -Decoding meshes\x\ex_v_ban_avs_01.nif -Decoding meshes\x\ex_v_2_stairs_01.nif -Decoding meshes\x\ex_t_turret_02.nif -Decoding meshes\x\ex_t_menhir_01.nif -Decoding meshes\x\ex_t_dormer_01.nif -Decoding meshes\x\ex_t_stair_01.nif -Decoding meshes\x\ex_t_root_stem.nif -Decoding meshes\x\ex_t_awning_02.nif -Decoding meshes\x\ex_t_manor_01.nif -Decoding meshes\x\ex_t_manor_02.nif -Decoding meshes\x\ex_t_dock_main.nif -Decoding meshes\x\ex_t_brace_01.nif -Decoding meshes\x\ex_t_turret_03.nif -Decoding meshes\x\ex_t_turret_01.nif -Decoding meshes\x\ex_t_dormer_02.nif -Decoding meshes\x\ex_t_awning_01.nif -Decoding meshes\x\ex_t_rootball.nif -Decoding meshes\x\ex_t_root_hook.nif -Decoding meshes\x\ex_vivec_h_02.nif -Decoding meshes\x\ex_vivec_h_12.nif -Decoding meshes\x\ex_vivec_c_02.nif -Decoding meshes\x\ex_vivec_g_02.nif -Decoding meshes\x\ex_vivec_w_02.nif -Decoding meshes\x\ex_v_banner_02.nif -Decoding meshes\x\ex_vivec_h_14.nif -Decoding meshes\x\ex_vivec_h_15.nif -Decoding meshes\x\ex_vivec_c_03.nif -Decoding meshes\x\ex_vivec_h_03.nif -Decoding meshes\x\ex_vivec_w_03.nif -Decoding meshes\x\ex_vivec_h_06.nif -Decoding meshes\x\ex_vivec_hf_02.nif -Decoding meshes\x\ex_vivec_hf_04.nif -Decoding meshes\x\ex_vivec_h_07.nif -Decoding meshes\x\ex_vivec_pq_02.nif -Decoding meshes\x\ex_vivec_pq_04.nif -Decoding meshes\x\ex_vivec_lp_02.nif -Decoding meshes\x\ex_vivec_h_05.nif -Decoding meshes\x\ex_vivec_h_17.nif -Decoding meshes\x\ex_vivec_h_16.nif -Decoding meshes\x\ex_vivec_t_04.nif -Decoding meshes\x\ex_vivec_h_04.nif -Decoding meshes\x\ex_vivec_c_04.nif -Decoding meshes\x\ex_v_bpost_01.nif -Decoding meshes\x\ex_v_bpost_03.nif -Decoding meshes\x\ex_v_stolms_01.nif -Decoding meshes\x\ex_v_bpost_02.nif -Decoding meshes\x\ex_vivec_h_09.nif -Decoding meshes\x\ex_v_banner_01.nif -Decoding meshes\x\ex_v_banner_03.nif -Decoding meshes\x\ex_vivec_w_01.nif -Decoding meshes\x\ex_vivec_t_01.nif -Decoding meshes\x\ex_vivec_h_11.nif -Decoding meshes\x\ex_vivec_h_01.nif -Decoding meshes\x\ex_vivec_g_01.nif -Decoding meshes\x\ex_vivec_b_01.nif -Decoding meshes\x\ex_vivec_c_01.nif -Decoding meshes\x\ex_vivec_h_10.nif -Decoding meshes\x\ex_vivec_h_13.nif -Decoding meshes\x\ex_v_1_wall_01.nif -Decoding meshes\x\ex_v_2_wall_01.nif -Decoding meshes\x\ex_vivec_hf_01.nif -Decoding meshes\x\ex_vivec_hf_03.nif -Decoding meshes\x\ex_vivec_pd_01.nif -Decoding meshes\x\ex_vivec_h_08.nif -Decoding meshes\x\ex_vivec_ps_01.nif -Decoding meshes\x\ex_vivec_pq_01.nif -Decoding meshes\x\ex_vivec_pq_03.nif -Decoding meshes\x\ex_vivec_lp_01.nif -Decoding meshes\x\ex_vivec_bt_01.nif -Decoding meshes\x\ex_rope_short.nif -Decoding meshes\x\ex_ship_plank.nif -Decoding meshes\x\ex_scrapwood04.nif -Decoding meshes\x\ex_scrapwood02.nif -Decoding meshes\x\ex_ship_stair.nif -Decoding meshes\x\ex_scrapwood05.nif -Decoding meshes\x\ex_scrapwood03.nif -Decoding meshes\x\ex_scrapwood01.nif -Decoding meshes\x\ex_dwrv_ruin50.nif -Decoding meshes\x\ex_dae_ruin_04.nif -Decoding meshes\x\ex_dae_ruin_02.nif -Decoding meshes\x\ex_dwrv_ruin20.nif -Decoding meshes\x\ex_de_shack_05.nif -Decoding meshes\x\ex_de_shack_03.nif -Decoding meshes\x\ex_de_shack_01.nif -Decoding meshes\x\ex_dwrv_wall20.nif -Decoding meshes\x\ex_dwrv_wall30.nif -Decoding meshes\x\ex_dwrv_wall50.nif -Decoding meshes\x\ex_dwrv_wall60.nif -Decoding meshes\x\ex_dwrv_wall40.nif -Decoding meshes\x\ex_dwrv_wall10.nif -Decoding meshes\x\ex_dae_claw_02.nif -Decoding meshes\x\ex_dwrv_ruin10.nif -Decoding meshes\x\ex_dwrv_pipe10.nif -Decoding meshes\x\ex_dwrv_ruin40.nif -Decoding meshes\x\ex_dwrv_ruin00.nif -Decoding meshes\x\ex_dae_ruin_01.nif -Decoding meshes\x\ex_dae_ruin_03.nif -Decoding meshes\x\ex_de_shack_04.nif -Decoding meshes\x\ex_de_shack_02.nif -Decoding meshes\x\ex_dwrv_wall00.nif -Decoding meshes\x\ex_de_sn_gate.nif -Decoding meshes\x\ex_dwrv_ruin30.nif -Decoding meshes\x\ex_dwrv_ruin60.nif -Decoding meshes\x\ex_dwrv_pipe00.nif -Decoding meshes\x\ex_dwrv_ruin80.nif -Decoding meshes\x\ex_dae_claw_01.nif -Decoding meshes\x\ex_de_rowboat.nif -Decoding meshes\x\ex_dwrv_ruin70.nif -Decoding meshes\x\ex_gg_fence_02.nif -Decoding meshes\x\ex_gg_fence_04.nif -Decoding meshes\x\ex_gg_pylon_01.nif -Decoding meshes\x\ex_gondola_01.nif -Decoding meshes\x\ex_gg_fence_03.nif -Decoding meshes\x\ex_gg_fence_01.nif -Decoding meshes\x\ex_gg_pylon_02.nif -Decoding meshes\x\ex_ashl_banner.nif -Decoding meshes\x\ex_coiled_rope.nif -Decoding meshes\x\ex_cave_sand00.nif -Decoding meshes\x\ex_cave_ash10.nif -Decoding meshes\x\ex_cave_ash00.nif -Decoding meshes\x\ex_lavaspark01.nif -Decoding meshes\x\ex_lavaspark03.nif -Decoding meshes\x\ex_longboat02.nif -Decoding meshes\x\ex_lavasteam00.nif -Decoding meshes\x\ex_lavaspark02.nif -Decoding meshes\x\ex_longboatwrk.nif -Decoding meshes\x\ex_longboat01.nif -Decoding meshes\x\ex_nord_win_01.nif -Decoding meshes\x\ex_nord_win_02.nif -Decoding meshes\x\ex_hlaalu_b_04.nif -Decoding meshes\x\ex_hlaalu_b_06.nif -Decoding meshes\x\ex_hlaalu_b_02.nif -Decoding meshes\x\ex_hlaalu_b_08.nif -Decoding meshes\x\ex_hlaalu_b_18.nif -Decoding meshes\x\ex_hlaalu_b_12.nif -Decoding meshes\x\ex_hlaalu_b_10.nif -Decoding meshes\x\ex_hlaalu_b_16.nif -Decoding meshes\x\ex_hlaalu_b_14.nif -Decoding meshes\x\ex_hlaalu_b_25.nif -Decoding meshes\x\ex_hlaalu_b_27.nif -Decoding meshes\x\ex_hlaalu_b_21.nif -Decoding meshes\x\ex_hlaalu_b_23.nif -Decoding meshes\x\ex_hlaalu_b_05.nif -Decoding meshes\x\ex_hlaalu_b_07.nif -Decoding meshes\x\ex_hlaalu_b_01.nif -Decoding meshes\x\ex_hlaalu_b_03.nif -Decoding meshes\x\ex_hlaalu_b_09.nif -Decoding meshes\x\ex_hlaalu_b_19.nif -Decoding meshes\x\ex_hlaalu_b_13.nif -Decoding meshes\x\ex_hlaalu_b_11.nif -Decoding meshes\x\ex_hlaalu_b_17.nif -Decoding meshes\x\ex_hlaalu_b_15.nif -Decoding meshes\x\ex_hlaalu_b_24.nif -Decoding meshes\x\ex_hlaalu_b_26.nif -Decoding meshes\x\ex_hlaalu_b_20.nif -Decoding meshes\x\ex_hlaalu_b_22.nif -Decoding meshes\x\ex_hlaalu_b_28.nif -Decoding meshes\x\ex_imp_wall_01.nif -Decoding meshes\x\ex_imp_keep_02.nif -Decoding meshes\x\ex_imp_plat_01.nif -Decoding meshes\x\ex_imp_pool_01.nif -Decoding meshes\x\ex_imp_dock_01.nif -Decoding meshes\x\ex_imp_keep_01.nif -Decoding meshes\x\ex_imp_dock_02.nif -Decoding meshes\x\ex_t_root_02.nif -Decoding meshes\x\ex_t_root_04.nif -Decoding meshes\x\ex_dae_azura.nif -Decoding meshes\x\ex_t_root_05.nif -Decoding meshes\x\ex_gg_ent_01.nif -Decoding meshes\x\ex_t_root_06.nif -Decoding meshes\x\ex_t_tunnel.nif -Decoding meshes\x\ex_t_root_01.nif -Decoding meshes\x\ex_t_pole_01.nif -Decoding meshes\x\ex_longboat.nif -Decoding meshes\x\ex_imp_plaza.nif -Decoding meshes\x\ex_t_root_03.nif -Decoding meshes\x\ex_cave_mt00.nif -Decoding meshes\x\ex_v_dock_01.nif -Decoding meshes\x\ex_t_dock_01.nif -Decoding meshes\f\ex_ashl_e_banner_r.nif -Decoding meshes\f\ex_ashl_a_banner_r.nif -Decoding meshes\f\ex_ashl_z_banner_r.nif -Decoding meshes\f\ex_ashl_u_banner_r.nif -Decoding meshes\f\ex_ashl_z_banner.nif -Decoding meshes\f\ex_ashl_a_banner.nif -Decoding meshes\f\ex_ashl_u_banner.nif -Decoding meshes\f\ex_ashl_e_banner.nif -Decoding meshes\f\ex_ashl_banner.nif -Decoding meshes\f\ex_boulder02.nif -Decoding meshes\f\ex_boulder04.nif -Decoding meshes\f\ex_boulder00.nif -Decoding meshes\f\ex_boulder05.nif -Decoding meshes\f\ex_boulder08.nif -Decoding meshes\f\ex_boulder07.nif -Decoding meshes\f\ex_boulder06.nif -Decoding meshes\f\ex_boulder03.nif -Decoding meshes\f\ex_boulder01.nif -Decoding meshes\d\ex_vivec_grate_01.nif -Decoding meshes\d\ex_emp_tower_01_b.nif -Decoding meshes\d\ex_emp_tower_01_a.nif -Decoding meshes\d\ex_imp_loaddoor_03.nif -Decoding meshes\d\ex_imp_loaddoor_02.nif -Decoding meshes\d\ex_imp_loaddoor_01.nif -Decoding meshes\d\in_t_s_plain_door.nif -Decoding meshes\d\in_v_s_trapdoor_02.nif -Decoding meshes\d\in_v_s_trapdoor_01.nif -Decoding meshes\d\in_v_s_jaildoor_01.nif -Decoding meshes\d\ex_v_cantondoor_01.nif -Decoding meshes\d\in_vivec_grate_01.nif -Decoding meshes\d\ex_common_door_01.nif -Decoding meshes\d\in_de_cabindoor.nif -Decoding meshes\d\in_t_door_small.nif -Decoding meshes\d\in_r_trapdoor_01.nif -Decoding meshes\d\in_c_door_arched.nif -Decoding meshes\d\in_h_trapdoor_01.nif -Decoding meshes\d\in_t_l_door_01.nif -Decoding meshes\d\in_r_s_door_01.nif -Decoding meshes\d\in_dae_door_01.nif -Decoding meshes\d\in_ar_door_01.nif -Decoding meshes\d\in_ci_door_01.nif -Decoding meshes\d\in_hlaalu_door.nif -Decoding meshes\d\ex_cave_door_01.nif -Decoding meshes\d\ex_de_ship_door.nif -Decoding meshes\d\ex_nord_door_01.nif -Decoding meshes\d\ex_nord_door_02.nif -Decoding meshes\d\ex_r_trapdoor_01.nif -Decoding meshes\d\ex_h_trapdoor_01.nif -Decoding meshes\d\ex_t_door_02.nif -Decoding meshes\d\ex_t_door_01.nif -Decoding meshes\l\light_6th_candle_05.nif -Decoding meshes\l\light_6th_candle_04.nif -Decoding meshes\l\light_6th_candle_06.nif -Decoding meshes\l\light_6th_candle_01.nif -Decoding meshes\l\light_6th_candle_03.nif -Decoding meshes\l\light_6th_candle_02.nif -Decoding meshes\base_anim_female.1st.nif -Decoding meshes\base_anim_female.nif -Decoding meshes\base_animkna.1st.nif -Decoding meshes\base_anim.1st.nif -Decoding meshes\anim_dancinggirl.nif -Decoding meshes\x\ex_common_tower_thatchedroof.nif -Decoding meshes\menu_scroll_hort_bar.nif -Decoding meshes\menu_scroll_vert_bar.nif -Decoding meshes\menu_scroll_scroller.nif -Decoding meshes\menu_rightbuttondown.nif -Decoding meshes\main_menu_button.nif -Decoding meshes\menu_button_frame.nif -Decoding meshes\menu_thick_border.nif -Decoding meshes\menu_rightbuttonup.nif -Decoding meshes\menu_scroll_slider.nif -Decoding meshes\menu_messagebox.nif -Decoding meshes\menu_icon_frame.nif -Decoding meshes\menu_head_block.nif -Decoding meshes\menu_thin_border.nif -Decoding meshes\menu_scroll_bar.nif -Decoding meshes\menu_contents.nif -Decoding meshes\menu_scroll_da.nif -Decoding meshes\menu_scroll_ua.nif -Decoding meshes\i\in_vs_pitfloor_01.nif -Decoding meshes\i\in_ar_shelltop_01.nif -Decoding meshes\i\in_ar_platform_10.nif -Decoding meshes\i\in_ar_platform_04.nif -Decoding meshes\i\in_ar_platform_05.nif -Decoding meshes\i\in_ar_platform_06.nif -Decoding meshes\i\in_ar_platform_07.nif -Decoding meshes\i\in_ar_platform_01.nif -Decoding meshes\i\in_ar_platform_02.nif -Decoding meshes\i\in_ar_platform_03.nif -Decoding meshes\i\in_ar_platform_08.nif -Decoding meshes\i\in_ar_platform_09.nif -Decoding meshes\i\in_hlaalu_hall_128.nif -Decoding meshes\i\in_hlaalu_door_01.nif -Decoding meshes\i\in_hlaalu_doorjamb.nif -Decoding meshes\i\in_t_council_beams.nif -Decoding meshes\i\in_t_balconyscreen.nif -Decoding meshes\i\in_dae_doorjamb_01.nif -Decoding meshes\i\in_mudcave2_s_end.nif -Decoding meshes\i\in_mudcave_stal10.nif -Decoding meshes\i\in_mudcave_stal00.nif -Decoding meshes\i\in_mudcave_stal30.nif -Decoding meshes\i\in_mudcave_stal20.nif -Decoding meshes\i\in_mudcave_stal50.nif -Decoding meshes\i\in_mudcave_stal40.nif -Decoding meshes\i\in_mudcave_exit00.nif -Decoding meshes\i\in_mudcave_form00.nif -Decoding meshes\i\in_mudcave_form10.nif -Decoding meshes\i\in_mudcave_form20.nif -Decoding meshes\i\in_mudcave_form30.nif -Decoding meshes\i\in_redoran_hut_01.nif -Decoding meshes\i\in_c_doorframe_01.nif -Decoding meshes\i\in_dagoth_bridge00.nif -Decoding meshes\i\in_dagoth_plate10.nif -Decoding meshes\i\in_dagoth_plate00.nif -Decoding meshes\i\in_dagoth_plate20.nif -Decoding meshes\i\in_r_guild_mage_01.nif -Decoding meshes\i\in_com_traptop_01.nif -Decoding meshes\i\in_com_wincover_02.nif -Decoding meshes\i\in_com_wincover_01.nif -Decoding meshes\i\in_vsl_pitbd_b_01.nif -Decoding meshes\i\in_vsl_pitmd_b_01.nif -Decoding meshes\i\in_moldcave2_s_00.nif -Decoding meshes\i\in_moldcave2_s_01.nif -Decoding meshes\i\in_moldcave2_s_02.nif -Decoding meshes\i\in_moldcave2_s_03.nif -Decoding meshes\i\in_moldcave2_s_04.nif -Decoding meshes\i\in_moldcave2_s_05.nif -Decoding meshes\i\in_moldcave2_s_06.nif -Decoding meshes\i\in_moldcave_exit00.nif -Decoding meshes\i\in_moldcave2_s_end.nif -Decoding meshes\i\in_moldcave4_s_00.nif -Decoding meshes\i\in_moldcave_stal00.nif -Decoding meshes\i\in_moldcave_stal10.nif -Decoding meshes\i\in_moldcave_stal20.nif -Decoding meshes\i\in_moldcave_stal30.nif -Decoding meshes\i\in_moldcave_stal40.nif -Decoding meshes\i\in_moldcave_stal50.nif -Decoding meshes\i\in_moldcave_form20.nif -Decoding meshes\i\in_moldcave_form30.nif -Decoding meshes\i\in_moldcave_form00.nif -Decoding meshes\i\in_moldcave_form10.nif -Decoding meshes\i\in_velothicave_01.nif -Decoding meshes\i\in_velothicave_03.nif -Decoding meshes\i\in_velothicave_02.nif -Decoding meshes\i\in_velothicave_04.nif -Decoding meshes\i\in_t_l_room_corner.nif -Decoding meshes\i\in_t_l_room_entry.nif -Decoding meshes\i\in_t_l_hall_corner.nif -Decoding meshes\i\in_t_l_room_floor.nif -Decoding meshes\i\in_t_l_doorjamb_01.nif -Decoding meshes\i\in_v_l_int_wall_01.nif -Decoding meshes\i\in_v_l_int_wall_02.nif -Decoding meshes\i\in_v_l_int_rail_01.nif -Decoding meshes\i\in_r_l_int_wall_01.nif -Decoding meshes\i\in_r_l_int_wall_02.nif -Decoding meshes\i\in_r_l_doorjamb_02.nif -Decoding meshes\i\in_r_l_int_rail_01.nif -Decoding meshes\i\in_r_l_int_arch_01.nif -Decoding meshes\i\in_bonecave2_s_08.nif -Decoding meshes\i\in_bonecave2_s_09.nif -Decoding meshes\i\in_bonecave2_s_00.nif -Decoding meshes\i\in_bonecave2_s_01.nif -Decoding meshes\i\in_bonecave2_s_02.nif -Decoding meshes\i\in_bonecave2_s_03.nif -Decoding meshes\i\in_bonecave2_s_04.nif -Decoding meshes\i\in_bonecave2_s_05.nif -Decoding meshes\i\in_bonecave2_s_06.nif -Decoding meshes\i\in_bonecave2_s_07.nif -Decoding meshes\i\in_bonecave2_s_end.nif -Decoding meshes\i\in_bonecave4_s_00.nif -Decoding meshes\i\in_bonecave_stal00.nif -Decoding meshes\i\in_bonecave_stal10.nif -Decoding meshes\i\in_bonecave_stal20.nif -Decoding meshes\i\in_bonecave_stal30.nif -Decoding meshes\i\in_bonecave_stal40.nif -Decoding meshes\i\in_bonecave_stal50.nif -Decoding meshes\i\in_bonecave_form20.nif -Decoding meshes\i\in_bonecave_form30.nif -Decoding meshes\i\in_bonecave_form00.nif -Decoding meshes\i\in_bonecave_form10.nif -Decoding meshes\i\in_bonecave_exit00.nif -Decoding meshes\i\in_impbig_4way_01.nif -Decoding meshes\i\in_impbig_wall_01.nif -Decoding meshes\i\in_impbig_blend_01.nif -Decoding meshes\i\in_c_plain_corner.nif -Decoding meshes\i\in_c_pillar_stone.nif -Decoding meshes\i\in_c_plain_endcap.nif -Decoding meshes\i\in_t_s_room_corner.nif -Decoding meshes\i\in_t_stairs_wiz_01.nif -Decoding meshes\i\in_t_stairs_wiz_03.nif -Decoding meshes\i\in_t_stairs_wiz_02.nif -Decoding meshes\i\in_t_s_room_entry.nif -Decoding meshes\i\in_t_s_shaft_6way.nif -Decoding meshes\i\in_t_s_djamb_plain.nif -Decoding meshes\i\in_t_s_room_center.nif -Decoding meshes\i\in_v_s_int_wall_01.nif -Decoding meshes\i\in_v_s_int_wall_03.nif -Decoding meshes\i\in_v_s_int_wall_02.nif -Decoding meshes\i\in_v_s_int_wall_04.nif -Decoding meshes\i\in_v_s_domeroom_01.nif -Decoding meshes\i\in_r_s_int_wall_01.nif -Decoding meshes\i\in_r_s_int_wall_02.nif -Decoding meshes\i\in_r_s_doorjamb_01.nif -Decoding meshes\i\in_r_s_int_rail_01.nif -Decoding meshes\i\in_r_s_int_rail_02.nif -Decoding meshes\i\in_r_s_int_arch_01.nif -Decoding meshes\i\in_r_s_int_arch_02.nif -Decoding meshes\i\in_c_stone_corner.nif -Decoding meshes\i\in_c_stone_endcap.nif -Decoding meshes\i\in_dwrv_doorjam00.nif -Decoding meshes\i\in_dwrv_oilslick00.nif -Decoding meshes\i\in_strong_corr2_03.nif -Decoding meshes\i\in_strong_corr2_02.nif -Decoding meshes\i\in_strong_corr2_01.nif -Decoding meshes\i\in_strong_corr2_00.nif -Decoding meshes\i\in_strong_corr2_04.nif -Decoding meshes\i\in_strong_corr4_04.nif -Decoding meshes\i\in_strong_corr4_03.nif -Decoding meshes\i\in_strong_corr4_02.nif -Decoding meshes\i\in_strong_corr4_01.nif -Decoding meshes\i\in_strong_corr4_00.nif -Decoding meshes\i\in_strong_corr1_00.nif -Decoding meshes\i\in_strong_rubble01.nif -Decoding meshes\i\in_strong_rubble00.nif -Decoding meshes\i\in_strong_corr3_02.nif -Decoding meshes\i\in_strong_corr3_01.nif -Decoding meshes\i\in_strong_corr3_00.nif -Decoding meshes\i\in_nord_ladder_02.nif -Decoding meshes\i\in_nord_ladder_01.nif -Decoding meshes\i\in_t_railing_pole.nif -Decoding meshes\i\in_t_root_wrap_02.nif -Decoding meshes\i\in_t_root_wrap_01.nif -Decoding meshes\i\in_t_t_room_center.nif -Decoding meshes\i\in_sewer_bcorr4_00.nif -Decoding meshes\i\in_sewer_scorr4_00.nif -Decoding meshes\i\in_sewer_bcorr2_03.nif -Decoding meshes\i\in_sewer_bcorr2_02.nif -Decoding meshes\i\in_sewer_bcorr2_01.nif -Decoding meshes\i\in_sewer_bcorr2_00.nif -Decoding meshes\i\in_sewer_bcorr2_05.nif -Decoding meshes\i\in_sewer_bcorr2_04.nif -Decoding meshes\i\in_sewer_scorr2_02.nif -Decoding meshes\i\in_sewer_scorr2_01.nif -Decoding meshes\i\in_sewer_scorr2_00.nif -Decoding meshes\i\in_sewer_anchor00.nif -Decoding meshes\i\in_sewer_pillar00.nif -Decoding meshes\i\in_sewer_bcorr3_00.nif -Decoding meshes\i\in_sewer_scorr1_00.nif -Decoding meshes\i\in_sewer_bcorr1_01.nif -Decoding meshes\i\in_sewer_bcorr1_00.nif -Decoding meshes\i\in_vivec_ent_s_01.nif -Decoding meshes\i\in_cavern_stairs00.nif -Decoding meshes\i\in_cavern_roots10.nif -Decoding meshes\i\in_cavern_roots00.nif -Decoding meshes\i\in_lavacave2_s_00.nif -Decoding meshes\i\in_lavacave2_s_01.nif -Decoding meshes\i\in_lavacave2_s_02.nif -Decoding meshes\i\in_lavacave2_s_03.nif -Decoding meshes\i\in_lavacave2_s_04.nif -Decoding meshes\i\in_lavacave2_s_05.nif -Decoding meshes\i\in_lavacave2_s_06.nif -Decoding meshes\i\in_lavacave_form20.nif -Decoding meshes\i\in_lavacave_form30.nif -Decoding meshes\i\in_lavacave_form00.nif -Decoding meshes\i\in_lavacave_form10.nif -Decoding meshes\i\in_lavacave_exit00.nif -Decoding meshes\i\in_lavacave2_s_end.nif -Decoding meshes\i\in_lavacave4_s_00.nif -Decoding meshes\i\in_lavacave_stal00.nif -Decoding meshes\i\in_lavacave_stal10.nif -Decoding meshes\i\in_lavacave_stal20.nif -Decoding meshes\i\in_lavacave_stal30.nif -Decoding meshes\i\in_lavacave_stal40.nif -Decoding meshes\i\in_lavacave_stal50.nif -Decoding meshes\i\in_pycave2_s_end.nif -Decoding meshes\i\in_pycave_stal10.nif -Decoding meshes\i\in_pycave_stal00.nif -Decoding meshes\i\in_pycave2_s_05.nif -Decoding meshes\i\in_pycave2_s_03.nif -Decoding meshes\i\in_pycave2_s_01.nif -Decoding meshes\i\in_pycave_stal30.nif -Decoding meshes\i\in_pycave_stal20.nif -Decoding meshes\i\in_pycave_stal50.nif -Decoding meshes\i\in_pycave_stal40.nif -Decoding meshes\i\in_pycave_exit00.nif -Decoding meshes\i\in_pycave4_s_00.nif -Decoding meshes\i\in_pycave2_s_06.nif -Decoding meshes\i\in_pycave2_s_04.nif -Decoding meshes\i\in_pycave2_s_02.nif -Decoding meshes\i\in_pycave2_s_00.nif -Decoding meshes\i\in_pycave_form00.nif -Decoding meshes\i\in_pycave_form10.nif -Decoding meshes\i\in_pycave_form20.nif -Decoding meshes\i\in_pycave_form30.nif -Decoding meshes\i\in_dwrv_scope50.nif -Decoding meshes\i\in_dwrv_shaft10.nif -Decoding meshes\i\in_dwrv_shaft00.nif -Decoding meshes\i\in_dwrv_crank00.nif -Decoding meshes\i\in_dwrv_scope20.nif -Decoding meshes\i\in_dwrv_obsrv10.nif -Decoding meshes\i\in_dwrv_corr4_01.nif -Decoding meshes\i\in_dwrv_corr4_00.nif -Decoding meshes\i\in_dwrv_corr4_03.nif -Decoding meshes\i\in_dwrv_corr4_02.nif -Decoding meshes\i\in_dwrv_corr4_05.nif -Decoding meshes\i\in_dwrv_corr4_04.nif -Decoding meshes\i\in_dwrv_corr2_01.nif -Decoding meshes\i\in_dwrv_corr2_00.nif -Decoding meshes\i\in_dwrv_corr2_03.nif -Decoding meshes\i\in_dwrv_corr2_02.nif -Decoding meshes\i\in_dwrv_corr2_05.nif -Decoding meshes\i\in_dwrv_corr2_04.nif -Decoding meshes\i\in_dwrv_corr2_07.nif -Decoding meshes\i\in_dwrv_corr2_06.nif -Decoding meshes\i\in_dwrv_corr3_01.nif -Decoding meshes\i\in_dwrv_corr3_00.nif -Decoding meshes\i\in_dwrv_corr3_02.nif -Decoding meshes\i\in_dwrv_corr1_00.nif -Decoding meshes\i\in_dwrv_corr2_30.nif -Decoding meshes\i\in_dwrv_scope40.nif -Decoding meshes\i\in_dwrv_hall4_03.nif -Decoding meshes\i\in_dwrv_hall4_02.nif -Decoding meshes\i\in_dwrv_hall4_01.nif -Decoding meshes\i\in_dwrv_hall4_00.nif -Decoding meshes\i\in_dwrv_hall2_00.nif -Decoding meshes\i\in_dwrv_hall3_02.nif -Decoding meshes\i\in_dwrv_hall3_00.nif -Decoding meshes\i\in_dwrv_scope30.nif -Decoding meshes\i\in_dwrv_scope00.nif -Decoding meshes\i\in_dwrv_obsrv00.nif -Decoding meshes\i\in_dwrv_scope10.nif -Decoding meshes\i\in_mudcave2_s_00.nif -Decoding meshes\i\in_mudcave2_s_01.nif -Decoding meshes\i\in_mudcave2_s_02.nif -Decoding meshes\i\in_mudcave2_s_03.nif -Decoding meshes\i\in_mudcave2_s_04.nif -Decoding meshes\i\in_mudcave2_s_05.nif -Decoding meshes\i\in_mudcave2_s_06.nif -Decoding meshes\i\in_mudcave4_s_00.nif -Decoding meshes\i\in_mudboulder04.nif -Decoding meshes\i\in_mudboulder00.nif -Decoding meshes\i\in_mudboulder02.nif -Decoding meshes\i\in_mudboulder05.nif -Decoding meshes\i\in_mudboulder01.nif -Decoding meshes\i\in_mudboulder03.nif -Decoding meshes\i\in_mudcave2_end.nif -Decoding meshes\i\in_mudcave_28_1.nif -Decoding meshes\i\in_mudcave_21_1.nif -Decoding meshes\i\in_strong_ramp10.nif -Decoding meshes\i\in_strong_ramp00.nif -Decoding meshes\i\in_strong_hall02.nif -Decoding meshes\i\in_strong_hall03.nif -Decoding meshes\i\in_strong_hall00.nif -Decoding meshes\i\in_strong_hall01.nif -Decoding meshes\i\in_strong_hall06.nif -Decoding meshes\i\in_strong_hall04.nif -Decoding meshes\i\in_strong_hall05.nif -Decoding meshes\i\in_vs_pitct_b_01.nif -Decoding meshes\i\in_vs_pitd_b_02.nif -Decoding meshes\i\in_vs_pittw_b_01.nif -Decoding meshes\i\in_vs_pitmw_b_01.nif -Decoding meshes\i\in_vs_pitb_b_01.nif -Decoding meshes\i\in_vs_pitbw_b_01.nif -Decoding meshes\i\in_vs_pitceil_01.nif -Decoding meshes\i\in_vs_pitcb_b_01.nif -Decoding meshes\i\in_vs_pitd_b_01.nif -Decoding meshes\i\in_vs_pitd_b_03.nif -Decoding meshes\i\in_vs_pittd_b_01.nif -Decoding meshes\i\in_vs_pitcm_b_01.nif -Decoding meshes\i\in_vsl_pitc_b_01.nif -Decoding meshes\i\in_ashl_door_01.nif -Decoding meshes\i\in_ashl_tent_05.nif -Decoding meshes\i\in_ashl_tent_03.nif -Decoding meshes\i\in_ashl_tent_01.nif -Decoding meshes\i\in_ashl_door_02.nif -Decoding meshes\i\in_ashl_tent_04.nif -Decoding meshes\i\in_ashl_tent_02.nif -Decoding meshes\i\in_ar_bridge_10.nif -Decoding meshes\i\in_ar_bridge_09.nif -Decoding meshes\i\in_ar_bridge_01.nif -Decoding meshes\i\in_ar_bridge_03.nif -Decoding meshes\i\in_ar_bridge_05.nif -Decoding meshes\i\in_ar_bridge_07.nif -Decoding meshes\i\in_ar_bridge_08.nif -Decoding meshes\i\in_ar_bridge_02.nif -Decoding meshes\i\in_ar_bridge_04.nif -Decoding meshes\i\in_ar_bridge_06.nif -Decoding meshes\i\in_bone_rock_25.nif -Decoding meshes\i\in_bone_rock_27.nif -Decoding meshes\i\in_bone_rock_21.nif -Decoding meshes\i\in_bone_rock_23.nif -Decoding meshes\i\in_bonecave2_11.nif -Decoding meshes\i\in_bonecave2_end.nif -Decoding meshes\i\in_bone_rock_18.nif -Decoding meshes\i\in_bone_rock_12.nif -Decoding meshes\i\in_bone_rock_10.nif -Decoding meshes\i\in_bone_rock_16.nif -Decoding meshes\i\in_bone_rock_14.nif -Decoding meshes\i\in_bonecave4_00.nif -Decoding meshes\i\in_bonecave2_02.nif -Decoding meshes\i\in_bonecave2_00.nif -Decoding meshes\i\in_bonecave2_06.nif -Decoding meshes\i\in_bonecave2_04.nif -Decoding meshes\i\in_bonecave2_08.nif -Decoding meshes\i\in_bone_rock_05.nif -Decoding meshes\i\in_bone_rock_07.nif -Decoding meshes\i\in_bone_rock_01.nif -Decoding meshes\i\in_bone_rock_03.nif -Decoding meshes\i\in_bone_rock_09.nif -Decoding meshes\i\in_boneboulder01.nif -Decoding meshes\i\in_boneboulder00.nif -Decoding meshes\i\in_boneboulder03.nif -Decoding meshes\i\in_boneboulder02.nif -Decoding meshes\i\in_boneboulder05.nif -Decoding meshes\i\in_boneboulder04.nif -Decoding meshes\i\in_bonetrans_00.nif -Decoding meshes\i\in_bone_rock_24.nif -Decoding meshes\i\in_bone_rock_26.nif -Decoding meshes\i\in_bone_rock_20.nif -Decoding meshes\i\in_bone_rock_22.nif -Decoding meshes\i\in_bone_rock_28.nif -Decoding meshes\i\in_bonecave_28_1.nif -Decoding meshes\i\in_bonecave2_10.nif -Decoding meshes\i\in_bonecave2_12.nif -Decoding meshes\i\in_bonecave_21_1.nif -Decoding meshes\i\in_bone_rock_19.nif -Decoding meshes\i\in_bone_rock_13.nif -Decoding meshes\i\in_bone_rock_11.nif -Decoding meshes\i\in_bone_rock_17.nif -Decoding meshes\i\in_bone_rock_15.nif -Decoding meshes\i\in_bonecave2_03.nif -Decoding meshes\i\in_bonecave2_01.nif -Decoding meshes\i\in_bonecave2_07.nif -Decoding meshes\i\in_bonecave2_05.nif -Decoding meshes\i\in_bonecave2_09.nif -Decoding meshes\i\in_bone_rock_04.nif -Decoding meshes\i\in_bone_rock_06.nif -Decoding meshes\i\in_bone_rock_02.nif -Decoding meshes\i\in_bone_rock_08.nif -Decoding meshes\i\in_moldcave_21_1.nif -Decoding meshes\i\in_mold_rock_25.nif -Decoding meshes\i\in_mold_rock_27.nif -Decoding meshes\i\in_mold_rock_21.nif -Decoding meshes\i\in_mold_rock_23.nif -Decoding meshes\i\in_mold_rock_19.nif -Decoding meshes\i\in_mold_rock_13.nif -Decoding meshes\i\in_mold_rock_11.nif -Decoding meshes\i\in_mold_rock_17.nif -Decoding meshes\i\in_mold_rock_15.nif -Decoding meshes\i\in_moldcave2_02.nif -Decoding meshes\i\in_moldcave2_00.nif -Decoding meshes\i\in_moldcave2_06.nif -Decoding meshes\i\in_moldcave2_04.nif -Decoding meshes\i\in_moldcave2_08.nif -Decoding meshes\i\in_moldcave4_00.nif -Decoding meshes\i\in_mold_rock_07.nif -Decoding meshes\i\in_mold_rock_05.nif -Decoding meshes\i\in_mold_rock_03.nif -Decoding meshes\i\in_mold_rock_01.nif -Decoding meshes\i\in_mold_rock_09.nif -Decoding meshes\i\in_moldtrans_00.nif -Decoding meshes\i\in_moldboulder05.nif -Decoding meshes\i\in_moldboulder04.nif -Decoding meshes\i\in_moldboulder03.nif -Decoding meshes\i\in_moldboulder02.nif -Decoding meshes\i\in_moldboulder01.nif -Decoding meshes\i\in_moldboulder00.nif -Decoding meshes\i\in_mold_rock_24.nif -Decoding meshes\i\in_mold_rock_26.nif -Decoding meshes\i\in_mold_rock_20.nif -Decoding meshes\i\in_mold_rock_22.nif -Decoding meshes\i\in_mold_rock_28.nif -Decoding meshes\i\in_moldcave2_end.nif -Decoding meshes\i\in_mold_rock_18.nif -Decoding meshes\i\in_mold_rock_12.nif -Decoding meshes\i\in_mold_rock_10.nif -Decoding meshes\i\in_mold_rock_16.nif -Decoding meshes\i\in_mold_rock_14.nif -Decoding meshes\i\in_moldcave2_03.nif -Decoding meshes\i\in_moldcave2_01.nif -Decoding meshes\i\in_moldcave2_07.nif -Decoding meshes\i\in_moldcave2_05.nif -Decoding meshes\i\in_moldcave2_09.nif -Decoding meshes\i\in_moldcave_28_1.nif -Decoding meshes\i\in_mold_rock_06.nif -Decoding meshes\i\in_mold_rock_04.nif -Decoding meshes\i\in_mold_rock_02.nif -Decoding meshes\i\in_mold_rock_08.nif -Decoding meshes\i\in_nord_house_03.nif -Decoding meshes\i\in_nord_house_02.nif -Decoding meshes\i\in_nord_house_01.nif -Decoding meshes\i\in_nord_house_05.nif -Decoding meshes\i\in_nord_house_04.nif -Decoding meshes\i\in_nord_doorf_01.nif -Decoding meshes\i\in_nord_doorf_02.nif -Decoding meshes\i\in_shipwreck_top.nif -Decoding meshes\i\in_sewer_canal00.nif -Decoding meshes\i\in_sewer_raft00.nif -Decoding meshes\i\in_sewer_union00.nif -Decoding meshes\i\in_de_shipcabin.nif -Decoding meshes\i\in_de_shack_door.nif -Decoding meshes\i\in_dae_hall_l_03.nif -Decoding meshes\i\in_dae_hall_l_02.nif -Decoding meshes\i\in_dae_hall_l_01.nif -Decoding meshes\i\in_cavern_ramp10.nif -Decoding meshes\i\in_cavern_ramp00.nif -Decoding meshes\i\in_cave_plant00.nif -Decoding meshes\i\in_cavern_beam40.nif -Decoding meshes\i\in_cavern_beam00.nif -Decoding meshes\i\in_cavern_beam10.nif -Decoding meshes\i\in_cavern_beam20.nif -Decoding meshes\i\in_cavern_beam30.nif -Decoding meshes\i\in_cave_plant10.nif -Decoding meshes\i\in_cavern_rail00.nif -Decoding meshes\i\in_cavern_rail10.nif -Decoding meshes\i\in_lava_rock_23.nif -Decoding meshes\i\in_lava_rock_21.nif -Decoding meshes\i\in_lava_rock_27.nif -Decoding meshes\i\in_lava_rock_25.nif -Decoding meshes\i\in_lavacave_21_1.nif -Decoding meshes\i\in_lavatrans_00.nif -Decoding meshes\i\in_lavacave2_end.nif -Decoding meshes\i\in_lava_rock_16.nif -Decoding meshes\i\in_lava_rock_14.nif -Decoding meshes\i\in_lava_rock_12.nif -Decoding meshes\i\in_lava_rock_10.nif -Decoding meshes\i\in_lava_rock_18.nif -Decoding meshes\i\in_lavacave2_05.nif -Decoding meshes\i\in_lavacave2_07.nif -Decoding meshes\i\in_lavacave2_01.nif -Decoding meshes\i\in_lavacave2_03.nif -Decoding meshes\i\in_lavacave2_09.nif -Decoding meshes\i\in_lava_rock_09.nif -Decoding meshes\i\in_lava_rock_05.nif -Decoding meshes\i\in_lava_rock_07.nif -Decoding meshes\i\in_lava_rock_01.nif -Decoding meshes\i\in_lava_rock_03.nif -Decoding meshes\i\in_lava_rock_28.nif -Decoding meshes\i\in_lava_rock_22.nif -Decoding meshes\i\in_lava_rock_20.nif -Decoding meshes\i\in_lava_rock_26.nif -Decoding meshes\i\in_lava_rock_24.nif -Decoding meshes\i\in_lavacave_28_1.nif -Decoding meshes\i\in_lava_1024_01.nif -Decoding meshes\i\in_lavaboulder02.nif -Decoding meshes\i\in_lavaboulder03.nif -Decoding meshes\i\in_lavaboulder00.nif -Decoding meshes\i\in_lavaboulder01.nif -Decoding meshes\i\in_lavaboulder04.nif -Decoding meshes\i\in_lavaboulder05.nif -Decoding meshes\i\in_lava_rock_17.nif -Decoding meshes\i\in_lava_rock_15.nif -Decoding meshes\i\in_lava_rock_13.nif -Decoding meshes\i\in_lava_rock_11.nif -Decoding meshes\i\in_lava_rock_19.nif -Decoding meshes\i\in_lavacave4_00.nif -Decoding meshes\i\in_lavacave2_04.nif -Decoding meshes\i\in_lavacave2_06.nif -Decoding meshes\i\in_lavacave2_00.nif -Decoding meshes\i\in_lavacave2_02.nif -Decoding meshes\i\in_lavacave2_08.nif -Decoding meshes\i\in_lava_rock_08.nif -Decoding meshes\i\in_lava_rock_04.nif -Decoding meshes\i\in_lava_rock_06.nif -Decoding meshes\i\in_lava_rock_02.nif -Decoding meshes\i\in_t_crystal_02.nif -Decoding meshes\i\in_t_s_shaft_01.nif -Decoding meshes\i\in_t_l_hall_3way.nif -Decoding meshes\i\in_t_housepod_01.nif -Decoding meshes\i\in_t_housepod_02.nif -Decoding meshes\i\in_t_s_pillar_04.nif -Decoding meshes\i\in_t_s_pillar_02.nif -Decoding meshes\i\in_t_s_hall_4way.nif -Decoding meshes\i\in_t_s_hall_3way.nif -Decoding meshes\i\in_t_councilhall.nif -Decoding meshes\i\in_t_gatesymbol.nif -Decoding meshes\i\in_t_l_pillar_01.nif -Decoding meshes\i\in_t_crystal_01.nif -Decoding meshes\i\in_t_l_room_side.nif -Decoding meshes\i\in_t_platform_01.nif -Decoding meshes\i\in_t_platform_02.nif -Decoding meshes\i\in_t_s_turret_03.nif -Decoding meshes\i\in_t_s_turret_02.nif -Decoding meshes\i\in_t_sidewall_01.nif -Decoding meshes\i\in_t_sidewall_02.nif -Decoding meshes\i\in_v_roomhol_01.nif -Decoding meshes\i\in_r_building_01.nif -Decoding meshes\i\in_c_stone_4way.nif -Decoding meshes\i\in_c_stone_3way.nif -Decoding meshes\i\in_c_railing_01.nif -Decoding meshes\i\in_c_wall_plain.nif -Decoding meshes\i\in_c_plain_4way.nif -Decoding meshes\i\in_c_plain_3way.nif -Decoding meshes\i\in_c_pillar_wood.nif -Decoding meshes\i\in_t_l_hall_01.nif -Decoding meshes\i\in_t_s_hall_01.nif -Decoding meshes\i\in_t_gasket_01.nif -Decoding meshes\i\in_t_manor_01.nif -Decoding meshes\i\in_t_manor_02.nif -Decoding meshes\i\in_t_l_door_01.nif -Decoding meshes\i\in_v_palace_01.nif -Decoding meshes\i\in_v_plaza_01.nif -Decoding meshes\i\in_v_roomhf_01.nif -Decoding meshes\i\in_v_arena_01.nif -Decoding meshes\i\in_v_plaza_02.nif -Decoding meshes\i\in_pytrans_00.nif -Decoding meshes\i\in_py_rock_15.nif -Decoding meshes\i\in_pycave_28_1.nif -Decoding meshes\i\in_pycave_21_1.nif -Decoding meshes\i\in_py_rock_19.nif -Decoding meshes\i\in_py_rock_09.nif -Decoding meshes\i\in_pycave2_06.nif -Decoding meshes\i\in_py_rock_02.nif -Decoding meshes\i\in_py_rock_12.nif -Decoding meshes\i\in_py_rock_22.nif -Decoding meshes\i\in_pycave2_00.nif -Decoding meshes\i\in_pycave4_00.nif -Decoding meshes\i\in_py_rock_05.nif -Decoding meshes\i\in_py_rock_23.nif -Decoding meshes\i\in_py_rock_03.nif -Decoding meshes\i\in_pycave2_01.nif -Decoding meshes\i\in_py_rock_06.nif -Decoding meshes\i\in_pycave2_07.nif -Decoding meshes\i\in_py_rock_16.nif -Decoding meshes\i\in_py_rock_20.nif -Decoding meshes\i\in_py_rock_10.nif -Decoding meshes\i\in_pycave2_04.nif -Decoding meshes\i\in_py_rock_26.nif -Decoding meshes\i\in_py_rock_27.nif -Decoding meshes\i\in_pyboulder04.nif -Decoding meshes\i\in_pyboulder00.nif -Decoding meshes\i\in_pyboulder02.nif -Decoding meshes\i\in_py_rock_25.nif -Decoding meshes\i\in_pycave2_03.nif -Decoding meshes\i\in_py_rock_07.nif -Decoding meshes\i\in_pycave2_02.nif -Decoding meshes\i\in_py_rock_24.nif -Decoding meshes\i\in_py_rock_17.nif -Decoding meshes\i\in_prison_ship.nif -Decoding meshes\i\in_pycave2_09.nif -Decoding meshes\i\in_py_rock_14.nif -Decoding meshes\i\in_py_rock_13.nif -Decoding meshes\i\in_pycave2_05.nif -Decoding meshes\i\in_pyboulder05.nif -Decoding meshes\i\in_pyboulder01.nif -Decoding meshes\i\in_pyboulder03.nif -Decoding meshes\i\in_py_rock_28.nif -Decoding meshes\i\in_py_rock_18.nif -Decoding meshes\i\in_pycave2_end.nif -Decoding meshes\i\in_py_rock_08.nif -Decoding meshes\i\in_py_rock_04.nif -Decoding meshes\i\in_pycave2_08.nif -Decoding meshes\i\in_py_rock_11.nif -Decoding meshes\i\in_py_rock_01.nif -Decoding meshes\i\in_py_rock_21.nif -Decoding meshes\i\in_r_s_room_02.nif -Decoding meshes\i\in_r_s_room_01.nif -Decoding meshes\i\in_r_s_room_03.nif -Decoding meshes\i\in_dwrv_gear10.nif -Decoding meshes\i\in_de_shack_05.nif -Decoding meshes\i\in_de_shack_03.nif -Decoding meshes\i\in_de_shack_01.nif -Decoding meshes\i\in_dwrv_gear00.nif -Decoding meshes\i\in_dwrv_wall10.nif -Decoding meshes\i\in_dwrv_lift00.nif -Decoding meshes\i\in_de_shack_04.nif -Decoding meshes\i\in_de_shack_02.nif -Decoding meshes\i\in_dwrv_wall00.nif -Decoding meshes\i\in_dwrv_gear20.nif -Decoding meshes\i\in_dae_claw_01.nif -Decoding meshes\i\in_dwrv_gear30.nif -Decoding meshes\i\in_akulacave00.nif -Decoding meshes\i\in_akulakhan00.nif -Decoding meshes\i\in_bonecave_17.nif -Decoding meshes\i\in_bonecave_15.nif -Decoding meshes\i\in_bonecave_13.nif -Decoding meshes\i\in_bonecave_11.nif -Decoding meshes\i\in_bonecave_19.nif -Decoding meshes\i\in_bonecave_21.nif -Decoding meshes\i\in_bonecave_27.nif -Decoding meshes\i\in_bonecave_25.nif -Decoding meshes\i\in_bonecave_08.nif -Decoding meshes\i\in_bonecave_06.nif -Decoding meshes\i\in_bonecave_04.nif -Decoding meshes\i\in_bonecave_02.nif -Decoding meshes\i\in_bonecave_00.nif -Decoding meshes\i\in_bonecave_16.nif -Decoding meshes\i\in_bonecave_14.nif -Decoding meshes\i\in_bonecave_12.nif -Decoding meshes\i\in_bonecave_10.nif -Decoding meshes\i\in_bonecave_18.nif -Decoding meshes\i\in_bonecave_26.nif -Decoding meshes\i\in_bonecave_09.nif -Decoding meshes\i\in_bonecave_07.nif -Decoding meshes\i\in_bonecave_05.nif -Decoding meshes\i\in_bonecave_03.nif -Decoding meshes\i\in_bonecave_01.nif -Decoding meshes\i\in_ci_mummy_04.nif -Decoding meshes\i\in_ci_mummy_06.nif -Decoding meshes\i\in_ci_mummy_02.nif -Decoding meshes\i\in_ci_azura_01.nif -Decoding meshes\i\in_c_wall_rich.nif -Decoding meshes\i\in_ci_mummy_05.nif -Decoding meshes\i\in_ci_mummy_01.nif -Decoding meshes\i\in_ci_mummy_03.nif -Decoding meshes\i\in_lavacave_17.nif -Decoding meshes\i\in_lavacave_15.nif -Decoding meshes\i\in_lavacave_13.nif -Decoding meshes\i\in_lavacave_11.nif -Decoding meshes\i\in_lavacave_19.nif -Decoding meshes\i\in_lavalayer00.nif -Decoding meshes\i\in_lavacave_08.nif -Decoding meshes\i\in_lavacave_06.nif -Decoding meshes\i\in_lavacave_04.nif -Decoding meshes\i\in_lavacave_02.nif -Decoding meshes\i\in_lavacave_00.nif -Decoding meshes\i\in_lavacave_21.nif -Decoding meshes\i\in_lavacave_27.nif -Decoding meshes\i\in_lavacave_25.nif -Decoding meshes\i\in_lavacave_16.nif -Decoding meshes\i\in_lavacave_14.nif -Decoding meshes\i\in_lavacave_12.nif -Decoding meshes\i\in_lavacave_10.nif -Decoding meshes\i\in_lavacave_18.nif -Decoding meshes\i\in_lavacave_09.nif -Decoding meshes\i\in_lavacave_07.nif -Decoding meshes\i\in_lavacave_05.nif -Decoding meshes\i\in_lavacave_03.nif -Decoding meshes\i\in_lavacave_01.nif -Decoding meshes\i\in_lavacave_26.nif -Decoding meshes\i\in_mudcave_14.nif -Decoding meshes\i\in_mudcave_07.nif -Decoding meshes\i\in_mudcave_17.nif -Decoding meshes\i\in_mudtrans_00.nif -Decoding meshes\i\in_mudcave_27.nif -Decoding meshes\i\in_moldcave_17.nif -Decoding meshes\i\in_moldcave_15.nif -Decoding meshes\i\in_moldcave_13.nif -Decoding meshes\i\in_moldcave_11.nif -Decoding meshes\i\in_moldcave_19.nif -Decoding meshes\i\in_mudcave_10.nif -Decoding meshes\i\in_mudcave_00.nif -Decoding meshes\i\in_mudcave_03.nif -Decoding meshes\i\in_mudcave2_08.nif -Decoding meshes\i\in_mudcave2_04.nif -Decoding meshes\i\in_mudcave2_06.nif -Decoding meshes\i\in_mudcave2_00.nif -Decoding meshes\i\in_mudcave2_02.nif -Decoding meshes\i\in_mudcave4_00.nif -Decoding meshes\i\in_mud_rock_21.nif -Decoding meshes\i\in_mud_rock_23.nif -Decoding meshes\i\in_mud_rock_25.nif -Decoding meshes\i\in_mud_rock_27.nif -Decoding meshes\i\in_mudcave_04.nif -Decoding meshes\i\in_mudcave_21.nif -Decoding meshes\i\in_mudcave_11.nif -Decoding meshes\i\in_mudcave_01.nif -Decoding meshes\i\in_mudcave_18.nif -Decoding meshes\i\in_mudcave_08.nif -Decoding meshes\i\in_mud_rock_13.nif -Decoding meshes\i\in_mud_rock_11.nif -Decoding meshes\i\in_mud_rock_17.nif -Decoding meshes\i\in_mud_rock_15.nif -Decoding meshes\i\in_mud_rock_19.nif -Decoding meshes\i\in_moldcave_21.nif -Decoding meshes\i\in_moldcave_27.nif -Decoding meshes\i\in_moldcave_25.nif -Decoding meshes\i\in_moldcave_08.nif -Decoding meshes\i\in_moldcave_06.nif -Decoding meshes\i\in_moldcave_04.nif -Decoding meshes\i\in_moldcave_02.nif -Decoding meshes\i\in_moldcave_00.nif -Decoding meshes\i\in_mud_rock_07.nif -Decoding meshes\i\in_mud_rock_05.nif -Decoding meshes\i\in_mud_rock_03.nif -Decoding meshes\i\in_mud_rock_01.nif -Decoding meshes\i\in_mud_rock_09.nif -Decoding meshes\i\in_mudcave_26.nif -Decoding meshes\i\in_moldcave_16.nif -Decoding meshes\i\in_moldcave_14.nif -Decoding meshes\i\in_moldcave_12.nif -Decoding meshes\i\in_moldcave_10.nif -Decoding meshes\i\in_moldcave_18.nif -Decoding meshes\i\in_mudcave_13.nif -Decoding meshes\i\in_mudcave_06.nif -Decoding meshes\i\in_mudcave_25.nif -Decoding meshes\i\in_mudcave2_09.nif -Decoding meshes\i\in_mudcave2_05.nif -Decoding meshes\i\in_mudcave2_07.nif -Decoding meshes\i\in_mudcave2_01.nif -Decoding meshes\i\in_mudcave2_03.nif -Decoding meshes\i\in_mudcave_16.nif -Decoding meshes\i\in_mud_rock_20.nif -Decoding meshes\i\in_mud_rock_22.nif -Decoding meshes\i\in_mud_rock_24.nif -Decoding meshes\i\in_mud_rock_26.nif -Decoding meshes\i\in_mud_rock_28.nif -Decoding meshes\i\in_mudcave_15.nif -Decoding meshes\i\in_mud_rock_12.nif -Decoding meshes\i\in_mud_rock_10.nif -Decoding meshes\i\in_mud_rock_16.nif -Decoding meshes\i\in_mud_rock_14.nif -Decoding meshes\i\in_mud_rock_18.nif -Decoding meshes\i\in_moldcave_26.nif -Decoding meshes\i\in_moldcave_09.nif -Decoding meshes\i\in_moldcave_07.nif -Decoding meshes\i\in_moldcave_05.nif -Decoding meshes\i\in_moldcave_03.nif -Decoding meshes\i\in_moldcave_01.nif -Decoding meshes\i\in_mud_rock_06.nif -Decoding meshes\i\in_mud_rock_04.nif -Decoding meshes\i\in_mud_rock_02.nif -Decoding meshes\i\in_mud_rock_08.nif -Decoding meshes\i\in_mudcave_05.nif -Decoding meshes\i\in_mudcave_02.nif -Decoding meshes\i\in_mudcave_12.nif -Decoding meshes\i\in_mudcave_19.nif -Decoding meshes\i\in_mudcave_09.nif -Decoding meshes\i\in_hlaalu_door.nif -Decoding meshes\i\in_hlaalu_wall.nif -Decoding meshes\i\in_icesheet_01.nif -Decoding meshes\i\in_icesheet_02.nif -Decoding meshes\i\in_pycave_15.nif -Decoding meshes\i\in_pycave_02.nif -Decoding meshes\i\in_pycave_12.nif -Decoding meshes\i\in_lava_256a.nif -Decoding meshes\i\in_pycave_05.nif -Decoding meshes\i\in_lava_oval.nif -Decoding meshes\i\in_pycave_06.nif -Decoding meshes\i\in_pycave_03.nif -Decoding meshes\i\in_pycave_16.nif -Decoding meshes\i\in_pycave_19.nif -Decoding meshes\i\in_pycave_26.nif -Decoding meshes\i\in_pycave_09.nif -Decoding meshes\i\in_pycave_27.nif -Decoding meshes\i\in_pycave_18.nif -Decoding meshes\i\in_pycave_25.nif -Decoding meshes\i\in_pycave_08.nif -Decoding meshes\i\in_pycave_10.nif -Decoding meshes\i\in_pycave_00.nif -Decoding meshes\i\in_pycave_07.nif -Decoding meshes\i\in_pycave_17.nif -Decoding meshes\i\in_lava_256.nif -Decoding meshes\i\in_t_l_4way.nif -Decoding meshes\i\in_t_edge_01.nif -Decoding meshes\i\in_pycave_14.nif -Decoding meshes\i\in_lava_512.nif -Decoding meshes\i\in_pycave_13.nif -Decoding meshes\i\in_lava_1024.nif -Decoding meshes\i\in_pycave_11.nif -Decoding meshes\i\in_pycave_01.nif -Decoding meshes\i\in_pycave_21.nif -Decoding meshes\i\in_pycave_04.nif -Decoding meshes\i\in_c_skywalk.nif -Decoding meshes\i\in_6th_chalk30.nif -Decoding meshes\i\in_6th_chalk20.nif -Decoding meshes\i\in_6th_chalk00.nif -Decoding meshes\i\in_6th_chalk10.nif -Decoding meshes\i\tx_crystal_03.nif -Decoding meshes\i\tx_crystal_02.nif -Decoding meshes\i\ex_dae_ruin_04.nif -Decoding meshes\i\ex_dae_ruin_02.nif -Decoding meshes\a\a_imperial_f_shoe.nif -Decoding meshes\a\a_imperial_a_boot.nif -Decoding meshes\a\a_iron_cuirass_gnd.nif -Decoding meshes\a\a_iron_pauldron_fa.nif -Decoding meshes\a\a_iron_pauldron_ua.nif -Decoding meshes\a\a_iron_pauldron_cl.nif -Decoding meshes\a\a_iron_greaves_gnd.nif -Decoding meshes\a\a_iron_greaves_ul.nif -Decoding meshes\a\a_indoril_m_helmet.nif -Decoding meshes\a\a_indoril_m_f_boot.nif -Decoding meshes\a\a_indoril_m_a_boot.nif -Decoding meshes\a\a_indoril_m_skins.nif -Decoding meshes\a\shield_art_auriel.nif -Decoding meshes\marker_creature.nif -Decoding meshes\marker_travel.nif -Decoding meshes\marker_temple.nif -Decoding meshes\marker_prison.nif -Decoding meshes\marker_radius.nif -Decoding meshes\marker_divine.nif -Decoding meshes\m\skeleton_key.nif -Decoding meshes\a\shield_bonemold.nif -Decoding meshes\a\shield_dwemer.nif -Decoding meshes\a\shield_daedric.nif -Decoding meshes\a\shield_dreugh.nif -Decoding meshes\a\shield_indoril.nif -Decoding meshes\a\shield_chitin.nif -Decoding meshes\a\shield_ebony.nif -Decoding meshes\a\shield_steel.nif -Decoding meshes\a\shield_glass.nif -Decoding meshes\a\shield_iron.nif -Decoding meshes\a\a_imperial_skins.nif -Decoding meshes\a\a_imperial_skirt.nif -Decoding meshes\a\a_iron_boot_gnd.nif -Decoding meshes\a\a_iron_greaves_g.nif -Decoding meshes\a\a_iron_greaves_k.nif -Decoding meshes\a\a_iron_bracer_w.nif -Decoding meshes\a\a_iron_hands.1st.nif -Decoding meshes\a\a_iron_boot_f.nif -Decoding meshes\a\a_iron_skinned.nif -Decoding meshes\a\a_iron_boot_a.nif -Decoding meshes\a\a_iron_helm_01.nif -Decoding meshes\f\flora_bc_grass_02.nif -Decoding meshes\f\flora_bc_grass_01.nif -Decoding meshes\f\flora_comberry_01.nif -Decoding meshes\f\flora_chokeweed_01.nif -Decoding meshes\f\flora_roobrush_01.nif -Decoding meshes\f\flora_corkbulb_01.nif -Decoding meshes\n\ingred_emerald_01.nif -Decoding meshes\n\ingred_comberry_01.nif -Decoding meshes\n\ingred_ash_yam_01.nif -Decoding meshes\n\ingred_russula_01.nif -Decoding meshes\n\ingred_coprinus_01.nif -Decoding meshes\n\ingred_scuttle_01.nif -Decoding meshes\n\ingred_saltrice_01.nif -Decoding meshes\n\ingred_roobrush_01.nif -Decoding meshes\n\ingred_heather_01.nif -Decoding meshes\n\ingred_rat_meat_01.nif -Decoding meshes\n\ingred_crabmeat_01.nif -Decoding meshes\n\ingred_rawebony_01.nif -Decoding meshes\n\ingred_diamond_01.nif -Decoding meshes\f\food_kwama_egg_01.nif -Decoding meshes\f\food_kwama_egg_02.nif -Decoding meshes\n\ingred_pearl_01.nif -Decoding meshes\n\ingred_bread_01.nif -Decoding meshes\n\ingred_scales_01.nif -Decoding meshes\n\ingred_resin_01.nif -Decoding meshes\n\ingred_muck_01.nif -Decoding meshes\n\ingred_ruby_01.nif -Decoding meshes\f\flora_root_wg_07.nif -Decoding meshes\f\flora_root_wg_06.nif -Decoding meshes\f\flora_root_wg_05.nif -Decoding meshes\f\flora_root_wg_04.nif -Decoding meshes\f\flora_root_wg_03.nif -Decoding meshes\f\flora_root_wg_02.nif -Decoding meshes\f\flora_root_wg_01.nif -Decoding meshes\f\flora_root_wg_08.nif -Decoding meshes\f\flora_bc_log_02.nif -Decoding meshes\f\flora_muckpod_04.nif -Decoding meshes\f\flora_muckpod_05.nif -Decoding meshes\f\flora_muckpod_02.nif -Decoding meshes\f\flora_muckpod_03.nif -Decoding meshes\f\flora_muckpod_01.nif -Decoding meshes\f\flora_bc_moss_03.nif -Decoding meshes\f\flora_bc_moss_02.nif -Decoding meshes\f\flora_bc_moss_01.nif -Decoding meshes\f\flora_bc_moss_07.nif -Decoding meshes\f\flora_bc_moss_06.nif -Decoding meshes\f\flora_bc_moss_05.nif -Decoding meshes\f\flora_bc_moss_04.nif -Decoding meshes\f\flora_bc_moss_09.nif -Decoding meshes\f\flora_bc_moss_08.nif -Decoding meshes\f\flora_bc_moss_13.nif -Decoding meshes\f\flora_bc_moss_12.nif -Decoding meshes\f\flora_bc_moss_11.nif -Decoding meshes\f\flora_bc_moss_10.nif -Decoding meshes\f\flora_bc_moss_17.nif -Decoding meshes\f\flora_bc_moss_16.nif -Decoding meshes\f\flora_bc_moss_15.nif -Decoding meshes\f\flora_bc_moss_14.nif -Decoding meshes\f\flora_bc_moss_19.nif -Decoding meshes\f\flora_bc_moss_18.nif -Decoding meshes\f\flora_bc_moss_21.nif -Decoding meshes\f\flora_bc_moss_20.nif -Decoding meshes\f\flora_bc_tree_13.nif -Decoding meshes\f\flora_bc_tree_12.nif -Decoding meshes\f\flora_bc_tree_11.nif -Decoding meshes\f\flora_bc_tree_10.nif -Decoding meshes\f\flora_bc_tree_03.nif -Decoding meshes\f\flora_bc_tree_02.nif -Decoding meshes\f\flora_bc_tree_01.nif -Decoding meshes\f\flora_bc_tree_07.nif -Decoding meshes\f\flora_bc_tree_06.nif -Decoding meshes\f\flora_bc_tree_05.nif -Decoding meshes\f\flora_bc_tree_04.nif -Decoding meshes\f\flora_bc_tree_09.nif -Decoding meshes\f\flora_bc_tree_08.nif -Decoding meshes\f\flora_bc_vine_02.nif -Decoding meshes\f\flora_bc_vine_03.nif -Decoding meshes\f\flora_bc_vine_01.nif -Decoding meshes\f\flora_bc_vine_06.nif -Decoding meshes\f\flora_bc_vine_07.nif -Decoding meshes\f\flora_bc_vine_04.nif -Decoding meshes\f\flora_bc_vine_05.nif -Decoding meshes\f\flora_bc_fern_04.nif -Decoding meshes\f\flora_bc_fern_03.nif -Decoding meshes\f\flora_bc_fern_02.nif -Decoding meshes\f\flora_bc_fern_01.nif -Decoding meshes\f\flora_bc_log_01.nif -Decoding meshes\f\flora_tree_gl_11.nif -Decoding meshes\f\flora_tree_gl_10.nif -Decoding meshes\f\flora_tree_ac_04.nif -Decoding meshes\f\flora_tree_ac_01.nif -Decoding meshes\f\flora_tree_ac_03.nif -Decoding meshes\f\flora_tree_ac_02.nif -Decoding meshes\f\flora_tree_wg_08.nif -Decoding meshes\f\flora_tree_wg_01.nif -Decoding meshes\f\flora_tree_wg_03.nif -Decoding meshes\f\flora_tree_wg_02.nif -Decoding meshes\f\flora_tree_wg_05.nif -Decoding meshes\f\flora_tree_wg_04.nif -Decoding meshes\f\flora_tree_wg_07.nif -Decoding meshes\f\flora_tree_wg_06.nif -Decoding meshes\f\flora_tree_ai_05.nif -Decoding meshes\f\flora_tree_ai_06.nif -Decoding meshes\f\flora_tree_gl_09.nif -Decoding meshes\f\flora_tree_gl_08.nif -Decoding meshes\f\flora_tree_gl_01.nif -Decoding meshes\f\flora_tree_gl_03.nif -Decoding meshes\f\flora_tree_gl_02.nif -Decoding meshes\f\flora_tree_gl_05.nif -Decoding meshes\f\flora_tree_gl_04.nif -Decoding meshes\f\flora_tree_gl_07.nif -Decoding meshes\f\flora_tree_gl_06.nif -Decoding meshes\f\flora_heather_01.nif -Decoding meshes\f\flora_bc_knee_04.nif -Decoding meshes\f\flora_bc_knee_01.nif -Decoding meshes\f\flora_bc_knee_03.nif -Decoding meshes\f\flora_bc_knee_02.nif -Decoding meshes\f\flora_tree_01.nif -Decoding meshes\f\flora_kelp_02.nif -Decoding meshes\f\flora_grass_04.nif -Decoding meshes\f\flora_grass_02.nif -Decoding meshes\f\flora_kelp_03.nif -Decoding meshes\f\flora_tree_02.nif -Decoding meshes\f\flora_tree_04.nif -Decoding meshes\f\flora_tree_03.nif -Decoding meshes\f\flora_grass_03.nif -Decoding meshes\f\flora_grass_01.nif -Decoding meshes\f\flora_kelp_01.nif -Decoding meshes\f\flora_bush_01.nif -Decoding meshes\f\flora_kelp_04.nif -Decoding meshes\f\flora_ivy_01.nif -Decoding meshes\f\flora_ivy_02.nif -Decoding meshes\a\a_helm_colovian.nif -Decoding meshes\a\a_helm_silver.nif -Decoding meshes\f\velothi_sewer_door.nif -Decoding meshes\x\terrain_rock_ma_5a.nif -Decoding meshes\x\terrain_rock_ma_39.nif -Decoding meshes\x\terrain_rock_ma_19.nif -Decoding meshes\x\terrain_rock_ma_09.nif -Decoding meshes\x\terrain_rock_ma_79.nif -Decoding meshes\x\terrain_rock_ma_59.nif -Decoding meshes\x\terrain_rock_ma_49.nif -Decoding meshes\x\terrain_rock_ma_58.nif -Decoding meshes\x\terrain_rock_ma_53.nif -Decoding meshes\x\terrain_rock_ma_43.nif -Decoding meshes\x\terrain_rock_ma_02.nif -Decoding meshes\x\terrain_rock_ma_01.nif -Decoding meshes\x\terrain_rock_ma_51.nif -Decoding meshes\x\terrain_rock_ma_10.nif -Decoding meshes\x\terrain_rock_ma_40.nif -Decoding meshes\x\terrain_rock_ma_07.nif -Decoding meshes\x\terrain_rock_ma_67.nif -Decoding meshes\x\terrain_rock_ma_57.nif -Decoding meshes\x\terrain_rock_ma_06.nif -Decoding meshes\x\terrain_rock_ma_56.nif -Decoding meshes\x\terrain_rock_ma_25.nif -Decoding meshes\x\terrain_rock_ma_05.nif -Decoding meshes\x\terrain_rock_ma_55.nif -Decoding meshes\x\terrain_rock_ma_04.nif -Decoding meshes\x\terrain_rock_ma_54.nif -Decoding meshes\x\terrain_ashmire_01.nif -Decoding meshes\x\terrain_ma_rock_51.nif -Decoding meshes\x\terrain_ma_rock_53.nif -Decoding meshes\x\terrain_ma_rock_13.nif -Decoding meshes\x\terrain_rock_ma31.nif -Decoding meshes\x\terrain_rock_ma41.nif -Decoding meshes\x\terrain_ma_rock13.nif -Decoding meshes\x\terrain_ma_bridge.nif -Decoding meshes\x\terrain_ma_rock22.nif -Decoding meshes\x\terrain_ma_rock32.nif -Decoding meshes\x\terrain_ma_rock62.nif -Decoding meshes\x\terrain_lavapot_02.nif -Decoding meshes\x\terrain_lavapot_03.nif -Decoding meshes\x\terrain_lava_vent.nif -Decoding meshes\x\furn_sign_arms_01.nif -Decoding meshes\x\furn_tikitorch_out.nif -Decoding meshes\x\furn_com_fence_02.nif -Decoding meshes\x\furn_com_fence_03.nif -Decoding meshes\x\furn_com_fence_01.nif -Decoding meshes\editormarker_box_01.nif -Decoding meshes\x\terrain_lavapot.nif -Decoding meshes\x\terrain_lava_pot.nif -Decoding meshes\x\terrain_volcano.nif -Decoding meshes\x\terrain_ma_60.nif -Decoding meshes\x\terrain_ma_20.nif -Decoding meshes\x\terrain_ma_30.nif -Decoding meshes\x\furn_sign_inn_01.nif -Decoding meshes\x\furn_flagpole_01.nif -Decoding meshes\x\furn_signbase_01.nif -Decoding meshes\e\frost_shield.nif -Decoding meshes\l\light_com_torch_01.nif -Decoding meshes\l\light_com_torch_02.nif -Decoding meshes\l\light_tikitorch00.nif -Decoding meshes\l\light_de_candle_08.nif -Decoding meshes\l\light_de_candle_18.nif -Decoding meshes\l\light_de_candle_09.nif -Decoding meshes\l\light_de_candle_19.nif -Decoding meshes\l\light_de_candle_24.nif -Decoding meshes\l\light_de_candle_04.nif -Decoding meshes\l\light_de_candle_14.nif -Decoding meshes\l\light_de_candle_25.nif -Decoding meshes\l\light_de_candle_05.nif -Decoding meshes\l\light_de_candle_15.nif -Decoding meshes\l\light_de_candle_26.nif -Decoding meshes\l\light_de_candle_06.nif -Decoding meshes\l\light_de_candle_16.nif -Decoding meshes\l\light_de_candle_07.nif -Decoding meshes\l\light_de_candle_17.nif -Decoding meshes\l\light_de_candle_20.nif -Decoding meshes\l\light_de_candle_10.nif -Decoding meshes\l\light_de_candle_21.nif -Decoding meshes\l\light_de_candle_01.nif -Decoding meshes\l\light_de_candle_11.nif -Decoding meshes\l\light_de_candle_22.nif -Decoding meshes\l\light_de_candle_02.nif -Decoding meshes\l\light_de_candle_12.nif -Decoding meshes\l\light_de_candle_23.nif -Decoding meshes\l\light_de_candle_03.nif -Decoding meshes\l\light_de_candle_13.nif -Decoding meshes\l\light_com_lamp_01.nif -Decoding meshes\l\light_com_lamp_02.nif -Decoding meshes\l\light_redware_lamp.nif -Decoding meshes\l\light_6th_brazier.nif -Decoding meshes\l\light_fire_nosmoke.nif -Decoding meshes\l\light_dwrv_neon00.nif -Decoding meshes\d\door_dwrv_double01.nif -Decoding meshes\d\door_dwrv_double00.nif -Decoding meshes\d\door_dwrv_loadup00.nif -Decoding meshes\d\door_dwrv_inner00.nif -Decoding meshes\d\door_dwrv_load00.nif -Decoding meshes\d\door_dwrv_main00.nif -Decoding meshes\l\light_dae_censer.nif -Decoding meshes\l\light_common_01.nif -Decoding meshes\l\light_logpile10.nif -Decoding meshes\l\light_pitfire00.nif -Decoding meshes\l\light_buglamp_01.nif -Decoding meshes\l\light_de_lamp_01.nif -Decoding meshes\l\light_de_lamp_03.nif -Decoding meshes\l\light_de_lamp_02.nif -Decoding meshes\l\light_de_lamp_05.nif -Decoding meshes\l\light_de_lamp_04.nif -Decoding meshes\l\light_de_lamp_07.nif -Decoding meshes\l\light_de_lamp_06.nif -Decoding meshes\l\light_de_lamp_09.nif -Decoding meshes\l\light_de_lamp_08.nif -Decoding meshes\l\light_dae_jet.nif -Decoding meshes\l\light_stand_01.nif -Decoding meshes\l\light_tikilamp.nif -Decoding meshes\l\light_torch10.nif -Decoding meshes\l\light_sconce10.nif -Decoding meshes\l\light_sconce00.nif -Decoding meshes\l\light_torch_01.nif -Decoding meshes\b\b_n_breton_m_skins.nif -Decoding meshes\b\b_n_breton_f_skins.nif -Decoding meshes\b\b_n_breton_f_ankle.nif -Decoding meshes\b\b_n_breton_m_ankle.nif -Decoding meshes\b\b_n_breton_m_groin.nif -Decoding meshes\b\b_n_breton_f_groin.nif -Decoding meshes\b\b_n_breton_f_wrist.nif -Decoding meshes\b\b_n_breton_m_wrist.nif -Decoding meshes\b\b_n_breton_f_neck.nif -Decoding meshes\b\b_n_breton_m_neck.nif -Decoding meshes\b\b_n_breton_f_foot.nif -Decoding meshes\b\b_n_breton_m_foot.nif -Decoding meshes\b\b_n_breton_f_knee.nif -Decoding meshes\b\b_n_breton_m_knee.nif -Decoding meshes\b\b_n_orc_f_forearm.nif -Decoding meshes\b\b_n_orc_m_forearm.nif -Decoding meshes\b\b_n_orc_m_head_04.nif -Decoding meshes\b\b_n_orc_m_head_01.nif -Decoding meshes\b\b_n_orc_m_head_02.nif -Decoding meshes\b\b_n_orc_m_head_03.nif -Decoding meshes\b\b_n_orc_m_hair_04.nif -Decoding meshes\b\b_n_orc_m_hair_05.nif -Decoding meshes\b\b_n_orc_m_hair_01.nif -Decoding meshes\b\b_n_orc_m_hair_02.nif -Decoding meshes\b\b_n_orc_m_hair_03.nif -Decoding meshes\b\b_n_orc_f_head_01.nif -Decoding meshes\b\b_n_orc_f_head_02.nif -Decoding meshes\b\b_n_orc_f_head_03.nif -Decoding meshes\a\a_m_chitin_a_boot.nif -Decoding meshes\a\a_m_chitin_skinned.nif -Decoding meshes\a\a_m_chitin_forearm.nif -Decoding meshes\a\a_m_chitin_f_boots.nif -Decoding meshes\a\a_m_chitin_helmet.nif -Decoding meshes\b\b_n_khajiit_m_knee.nif -Decoding meshes\b\b_n_khajiit_f_knee.nif -Decoding meshes\b\b_n_khajiit_m_neck.nif -Decoding meshes\b\b_n_khajiit_f_neck.nif -Decoding meshes\b\b_n_nord_m_hair03.nif -Decoding meshes\b\b_n_nord_m_hair02.nif -Decoding meshes\b\b_n_nord_m_hair01.nif -Decoding meshes\b\b_n_nord_m_hair00.nif -Decoding meshes\b\b_n_nord_m_hair07.nif -Decoding meshes\b\b_n_nord_m_hair06.nif -Decoding meshes\b\b_n_nord_m_hair05.nif -Decoding meshes\b\b_n_nord_m_hair04.nif -Decoding meshes\b\b_n_nord_f_head_01.nif -Decoding meshes\b\b_n_nord_m_head_01.nif -Decoding meshes\b\b_n_nord_m_head_02.nif -Decoding meshes\b\b_n_nord_f_head_03.nif -Decoding meshes\b\b_n_nord_m_head_03.nif -Decoding meshes\b\b_n_nord_f_head_02.nif -Decoding meshes\b\b_n_nord_m_head_04.nif -Decoding meshes\b\b_n_nord_f_head_05.nif -Decoding meshes\b\b_n_nord_m_head_05.nif -Decoding meshes\b\b_n_nord_f_head_04.nif -Decoding meshes\b\b_n_nord_m_head_06.nif -Decoding meshes\b\b_n_nord_f_head_07.nif -Decoding meshes\b\b_n_nord_m_head_07.nif -Decoding meshes\b\b_n_nord_f_head_06.nif -Decoding meshes\b\b_n_nord_m_head_08.nif -Decoding meshes\b\b_n_nord_f_head_08.nif -Decoding meshes\b\b_n_nord_f_hair_03.nif -Decoding meshes\b\b_n_nord_f_hair_02.nif -Decoding meshes\b\b_n_nord_f_hair_01.nif -Decoding meshes\b\b_n_nord_f_hair_05.nif -Decoding meshes\b\b_n_nord_f_hair_04.nif -Decoding meshes\b\b_n_nord_f_forearm.nif -Decoding meshes\b\b_n_nord_m_forearm.nif -Decoding meshes\cursor_drop_ground.nif -Decoding meshes\a\a_moragtong_helm.nif -Decoding meshes\b\b_n_nord_f_foot.nif -Decoding meshes\b\b_n_nord_m_ankle.nif -Decoding meshes\b\b_n_nord_m_foot.nif -Decoding meshes\b\b_n_nord_f_ankle.nif -Decoding meshes\b\b_n_nord_m_knee.nif -Decoding meshes\b\b_n_nord_f_knee.nif -Decoding meshes\b\b_n_nord_m_skins.nif -Decoding meshes\b\b_n_nord_m_neck.nif -Decoding meshes\b\b_n_nord_f_skins.nif -Decoding meshes\b\b_n_nord_m_groin.nif -Decoding meshes\b\b_n_nord_f_neck.nif -Decoding meshes\b\b_n_nord_f_wrist.nif -Decoding meshes\b\b_n_nord_m_wrist.nif -Decoding meshes\b\b_n_nord_f_groin.nif -Decoding meshes\b\b_n_orc_m_wrist.nif -Decoding meshes\b\b_n_orc_f_groin.nif -Decoding meshes\b\b_n_orc_m_groin.nif -Decoding meshes\b\b_n_orc_f_hair03.nif -Decoding meshes\b\b_n_orc_f_hair02.nif -Decoding meshes\b\b_n_orc_f_hair01.nif -Decoding meshes\b\b_n_orc_f_hair05.nif -Decoding meshes\b\b_n_orc_f_hair04.nif -Decoding meshes\b\b_n_orc_f_wrist.nif -Decoding meshes\b\b_n_orc_m_skins.nif -Decoding meshes\b\b_n_orc_f_skins.nif -Decoding meshes\b\b_n_orc_f_ankle.nif -Decoding meshes\b\b_n_orc_m_ankle.nif -Decoding meshes\a\a_molecrab_helm.nif -Decoding meshes\b\b_n_orc_f_knee.nif -Decoding meshes\b\b_n_orc_f_neck.nif -Decoding meshes\b\b_n_orc_f_foot.nif -Decoding meshes\b\b_n_orc_m_foot.nif -Decoding meshes\b\b_n_orc_m_knee.nif -Decoding meshes\b\b_n_orc_m_neck.nif -Decoding meshes\chimney_smoke_green.nif -Decoding meshes\chimney_smoke_small.nif -Decoding meshes\chimney_smoke02.nif -Decoding meshes\a\a_orcish_greaves_g.nif -Decoding meshes\a\a_orcish_greaves_k.nif -Decoding meshes\a\a_orcish_bracer_w.nif -Decoding meshes\a\a_orcish_cuirass_c.nif -Decoding meshes\a\a_orcish_boots_gnd.nif -Decoding meshes\c\c_m_robe_common_3.nif -Decoding meshes\c\c_m_robe_common_4.nif -Decoding meshes\c\c_m_robe_common_5.nif -Decoding meshes\c\c_m_robe_extrav_1c.nif -Decoding meshes\c\c_m_robe_extrav_1b.nif -Decoding meshes\c\c_m_robe_extrav_1a.nif -Decoding meshes\c\c_m_robe_extrav_1h.nif -Decoding meshes\c\c_m_robe_extrav_1t.nif -Decoding meshes\c\c_m_robe_extrav_1r.nif -Decoding meshes\c\c_m_robe_common_01.nif -Decoding meshes\c\c_m_robe_common_02.nif -Decoding meshes\c\c_m_robe_extrav_2.nif -Decoding meshes\c\c_m_robe_extrav_1.nif -Decoding meshes\c\c_m_robe_expens_3.nif -Decoding meshes\m\pick_apprentice_01.nif -Decoding meshes\m\pick_journeyman_01.nif -Decoding meshes\m\pick_master_01.nif -Decoding meshes\a\a_orcish_helmet.nif -Decoding meshes\a\a_orcish_boots_f.nif -Decoding meshes\a\a_orcish_boots_a.nif -Decoding meshes\a\a_newstscale_c_gnd.nif -Decoding meshes\a\a_netch_m_cuirass2.nif -Decoding meshes\a\a_netch_m_boot_gnd.nif -Decoding meshes\a\a_netch_m_greave_g.nif -Decoding meshes\a\a_netch_m_skinned.nif -Decoding meshes\a\a_nordicfur_boot_f.nif -Decoding meshes\a\a_nordicfur_boot_a.nif -Decoding meshes\a\a_nordiciron_helm.nif -Decoding meshes\a\a_nordicfur_helmet.nif -Decoding meshes\a\a_nordiciron_c_gnd.nif -Decoding meshes\argonian_swimkna.nif -Decoding meshes\a\a_netch_m_helmet.nif -Decoding meshes\a\a_netch_m_boot_f.nif -Decoding meshes\a\a_netch_m_boot_a.nif -Decoding meshes\a\a_nordiciron_c.nif -Decoding meshes\a\a_apostle_boots_f.nif -Decoding meshes\o\flora_saltrice_02.nif -Decoding meshes\o\flora_saltrice_01.nif -Decoding meshes\o\flora_fire_fern_01.nif -Decoding meshes\o\flora_fire_fern_02.nif -Decoding meshes\o\flora_fire_fern_03.nif -Decoding meshes\o\flora_chokeweed_01.nif -Decoding meshes\o\flora_roobrush_01.nif -Decoding meshes\o\flora_wickwheat_03.nif -Decoding meshes\o\flora_wickwheat_02.nif -Decoding meshes\o\flora_wickwheat_01.nif -Decoding meshes\o\flora_wickwheat_04.nif -Decoding meshes\o\flora_kreshweed_03.nif -Decoding meshes\o\flora_kreshweed_02.nif -Decoding meshes\o\flora_kreshweed_01.nif -Decoding meshes\o\flora_hackle-lo_01.nif -Decoding meshes\o\flora_hackle-lo_02.nif -Decoding meshes\a\a_art_wraithguard.nif -Decoding meshes\box.nif -Decoding meshes\r\corprus_stalker.nif -Decoding meshes\o\flora_ash_yam_02.nif -Decoding meshes\o\flora_ash_yam_01.nif -Decoding meshes\r\xgreatbonewalker.nif -Decoding meshes\r\atronach_fire.nif -Decoding meshes\r\atronach_storm.nif -Decoding meshes\r\atronach_frost.nif -Decoding meshes\a\a_art_dragon_gnd.nif -Decoding meshes\w\w_wakizashi_iron.nif -Decoding meshes\w\w_waraxe_daedric.nif -Decoding meshes\w\w_warhammer_iron.nif -Decoding meshes\w\w_waraxe_glass.nif -Decoding meshes\w\w_waraxe_iron.nif -Decoding meshes\w\w_waraxe_ebony.nif -Decoding meshes\w\w_waraxe_steel.nif -Decoding meshes\w\w_wooden_staff.nif -Decoding meshes\w\w_wakizashi.nif -Decoding meshes\w\w_warhammer.nif -Decoding meshes\c\c_belt_exquisite_1.nif -Decoding meshes\c\c_belt_expensive_1.nif -Decoding meshes\c\c_belt_expensive_3.nif -Decoding meshes\c\c_belt_expensive_2.nif -Decoding meshes\o\contain_drawer_02.nif -Decoding meshes\o\contain_drawer_03.nif -Decoding meshes\o\contain_drawer_01.nif -Decoding meshes\o\contain_de_desk_01.nif -Decoding meshes\o\contain_barrel_01.nif -Decoding meshes\o\contain_couldron10.nif -Decoding meshes\o\contain_corpse20.nif -Decoding meshes\o\contain_corpse00.nif -Decoding meshes\o\contain_corpse10.nif -Decoding meshes\o\contain_chest10.nif -Decoding meshes\o\contain_barrel10.nif -Decoding meshes\o\contain_crate_01.nif -Decoding meshes\o\contain_crate_02.nif -Decoding meshes\o\contain_chest11.nif -Decoding meshes\o\contain_sack00.nif -Decoding meshes\o\contain_urn_04.nif -Decoding meshes\o\contain_urn_02.nif -Decoding meshes\o\contain_urn_05.nif -Decoding meshes\o\contain_urn_01.nif -Decoding meshes\o\contain_urn_03.nif -Decoding meshes\o\contain_pot_01.nif -Decoding meshes\r\xascendedsleeper.nif -Decoding meshes\r\xashvampire.nif -Decoding meshes\c\c_belt_common_4.nif -Decoding meshes\c\c_belt_common_2.nif -Decoding meshes\c\c_belt_common_5.nif -Decoding meshes\c\c_belt_common_1.nif -Decoding meshes\c\c_belt_common_3.nif -Decoding meshes\c\c_belt_erabin.nif -Decoding meshes\e\magic_area_conjure.nif -Decoding meshes\e\magic_cast_poison.nif -Decoding meshes\e\magic_cast_conjure.nif -Decoding meshes\e\magic_cast_restore.nif -Decoding meshes\e\magic_cast_fortify.nif -Decoding meshes\e\magic_hit_levitate.nif -Decoding meshes\e\magic_area_poison.nif -Decoding meshes\e\magic_hit_conjure.nif -Decoding meshes\a\a_cephalopod_helm.nif -Decoding meshes\r\xsphere_centurions.nif -Decoding meshes\c\c_art_ring_mentor.nif -Decoding meshes\c\c_art_ring_khajiit.nif -Decoding meshes\c\c_art_ring_warlock.nif -Decoding meshes\e\lightning_shield.nif -Decoding meshes\e\lightningbolts.nif -Decoding meshes\e\lightning_area.nif -Decoding meshes\c\c_m_shirt_expensive_1_u_gnd.nif -Decoding meshes\e\magic_area_rest.nif -Decoding meshes\e\magic_hit_frost.nif -Decoding meshes\e\magic_area_drain.nif -Decoding meshes\e\magic_hit_poison.nif -Decoding meshes\e\magic_area_myst.nif -Decoding meshes\e\magic_cast_myst.nif -Decoding meshes\e\magic_area_frost.nif -Decoding meshes\e\magic_cast_frost.nif -Decoding meshes\e\magic_hit_alter.nif -Decoding meshes\e\magic_hit_ill.nif -Decoding meshes\e\magic_hit_rest.nif -Decoding meshes\e\magic_hit_dst.nif -Decoding meshes\e\magic_reflect.nif -Decoding meshes\e\magic_area_ill.nif -Decoding meshes\e\magic_area_alt.nif -Decoding meshes\e\magic_cast_ill.nif -Decoding meshes\e\magic_cast_alt.nif -Decoding meshes\e\magic_cast_dst.nif -Decoding meshes\e\magic_area_dst.nif -Decoding meshes\e\magic_hit_myst.nif -Decoding meshes\e\magic_hit_s.nif -Decoding meshes\e\magic_cast_l.nif -Decoding meshes\e\magic_cast_s.nif -Decoding meshes\e\magic_summon.nif -Decoding meshes\m\probe_master_01.nif -Decoding meshes\c\c_art_ring_wind.nif -Decoding meshes\a\a_bonemold_boots_a.nif -Decoding meshes\a\a_bonemold_boots_f.nif -Decoding meshes\a\a_bonemold_helmet.nif -Decoding meshes\w\w_tanto_daedric.nif -Decoding meshes\w\w_tanto_iron.nif -Decoding meshes\w\w_shortbow_chitin.nif -Decoding meshes\w\w_shortsword_ebony.nif -Decoding meshes\w\w_silver_claymore.nif -Decoding meshes\c\c_glove_expensive1.nif -Decoding meshes\c\c_glove_moragtong.nif -Decoding meshes\w\w_steel_battleaxe.nif -Decoding meshes\a\a_ebony_greaves_ul.nif -Decoding meshes\a\a_ebony_greaves_k.nif -Decoding meshes\a\a_ebony_g_greaves.nif -Decoding meshes\r\cavemudcrab.nif -Decoding meshes\w\w_staff_daedric.nif -Decoding meshes\w\w_spear_daedric.nif -Decoding meshes\w\w_silver_dagger.nif -Decoding meshes\w\w_silver_waraxe.nif -Decoding meshes\w\w_shortbow_steel.nif -Decoding meshes\a\a_ebony_cuirass.nif -Decoding meshes\a\a_ebony_bracer_w.nif -Decoding meshes\a\a_ebony_boot_gnd.nif -Decoding meshes\c\c_glove_common1.nif -Decoding meshes\w\w_staff_ebony.nif -Decoding meshes\w\w_steel_arrow.nif -Decoding meshes\w\w_steel_star .nif -Decoding meshes\w\w_steel_knife.nif -Decoding meshes\w\w_staff_glass.nif -Decoding meshes\w\w_shortsword00.nif -Decoding meshes\w\w_silver_staff.nif -Decoding meshes\w\w_silver_arrow.nif -Decoding meshes\w\w_silver_star.nif -Decoding meshes\w\w_silver_spear.nif -Decoding meshes\w\w_star_ebony.nif -Decoding meshes\w\w_saber_iron.nif -Decoding meshes\w\w_spikedclub.nif -Decoding meshes\w\w_star_glass.nif -Decoding meshes\w\w_spear_iron.nif -Decoding meshes\a\a_ebony_helmet.nif -Decoding meshes\a\a_ebony_boot_a.nif -Decoding meshes\a\a_ebony_boot_f.nif -Decoding meshes\a\a_daedric_boots_f.nif -Decoding meshes\a\a_daedric_boots_a.nif -Decoding meshes\a\a_dragonscale_helm.nif -Decoding meshes\a\a_dwemer_greaves_g.nif -Decoding meshes\a\a_dwemer_boots_gnd.nif -Decoding meshes\a\a_dwemer_cuirass_c.nif -Decoding meshes\a\a_dwemer_cuir_gnd.nif -Decoding meshes\a\a_dwemer_bracer_w.nif -Decoding meshes\f\xact_banner_khull.nif -Decoding meshes\r\xkwama worker.nif -Decoding meshes\r\xkwama warior.nif -Decoding meshes\r\xkwama forager.nif -Decoding meshes\r\xkwama queen.nif -Decoding meshes\i\in_dae_hall_l_stair_curve_01.nif -Decoding meshes\i\in_dae_hall_l_staircurve_01.nif -Decoding meshes\r\xdwarvenspecter.nif -Decoding meshes\f\xact_banner_vos.nif -Decoding meshes\c\c_m_pants_expensive_1_u_gnd.nif -Decoding meshes\i\in_t_s_hallshaft_ceilingcap.nif -Decoding meshes\a\a_dustadept_helm.nif -Decoding meshes\a\a_dwemer_boots_f.nif -Decoding meshes\a\a_dwemer_boots_a.nif -Decoding meshes\a\a_dwemer_helmet.nif -Decoding meshes\a\a_dreugh_cuirass.nif -Decoding meshes\a\a_daedric_skins.nif -Decoding meshes\a\a_daedric_god_h.nif -Decoding meshes\a\a_dreugh_helm.nif -Decoding meshes\a\a_glass_bracer_gnd.nif -Decoding meshes\a\a_glass_greaves_ul.nif -Decoding meshes\a\a_glass_greaves_g.nif -Decoding meshes\a\a_glass_greaves_k.nif -Decoding meshes\a\a_glass_boots_gnd.nif -Decoding meshes\r\xsteam_centurions.nif -Decoding meshes\r\netch_betty.nif -Decoding meshes\r\xatronach_storm.nif -Decoding meshes\r\xatronach_frost.nif -Decoding meshes\r\xatronach_fire.nif -Decoding meshes\a\a_glass_cuirass.nif -Decoding meshes\a\a_glass_boots_a.nif -Decoding meshes\a\a_glass_bracer_w.nif -Decoding meshes\a\a_glass_boots_f.nif -Decoding meshes\a\a_glass_helmet.nif -Decoding meshes\a\a_fur_cuirass_gnd.nif -Decoding meshes\i\xin_akulakhan00.nif -Decoding meshes\r\xguar_withpack.nif -Decoding meshes\r\xguar_white.nif -Decoding meshes\inventory_window.nif -Decoding meshes\a\a_fur_cuirass.nif -Decoding meshes\w\w_orcish_battleaxe.nif -Decoding meshes\w\w_orcish_warhammer.nif -Decoding meshes\w\w_nordic_claymore.nif -Decoding meshes\w\w_nordic_battleaxe.nif -Decoding meshes\w\w_n_claymore.nif -Decoding meshes\i\test _cavern_i_01.nif -Decoding meshes\r\sphere_centurions.nif -Decoding meshes\n\potion_skooma_01.nif -Decoding meshes\w\w_mace_daedric.nif -Decoding meshes\w\w_mace_ebony.nif -Decoding meshes\w\w_miner_pick.nif -Decoding meshes\w\w_mace_iron.nif -Decoding meshes\w\w_longbow_daedric.nif -Decoding meshes\w\w_longsword_silver.nif -Decoding meshes\w\w_longspear_ebony.nif -Decoding meshes\w\w_longbow_bonemold.nif -Decoding meshes\w\w_longsword_ebony.nif -Decoding meshes\r\xwingedtwilight.nif -Decoding meshes\r\xsiltstrider.nif -Decoding meshes\w\w_longbow_ariel.nif -Decoding meshes\w\w_longbow_steel.nif -Decoding meshes\r\wingedtwilight.nif -Decoding meshes\o\misc_sack00.nif -Decoding meshes\o\misc_chest11.nif -Decoding meshes\r\xancestorghost.nif -Decoding meshes\w\w_katana_daedric.nif -Decoding meshes\w\w_knife_glass.nif -Decoding meshes\w\w_knife_iron.nif -Decoding meshes\i\active_port_valen.nif -Decoding meshes\i\active_port_falen.nif -Decoding meshes\i\active_port_maran.nif -Decoding meshes\i\active_port_beran.nif -Decoding meshes\i\active_port_telas.nif -Decoding meshes\i\active_port_falag.nif -Decoding meshes\i\active_port_falas.nif -Decoding meshes\i\active_dag_port10.nif -Decoding meshes\i\active_port_andra.nif -Decoding meshes\i\in_c_stair_thatch_pend_tall_01.nif -Decoding meshes\i\in_c_stair_thatch_pend_tall_02.nif -Decoding meshes\e\vfx_pattern06.nif -Decoding meshes\e\vfx_pattern07.nif -Decoding meshes\e\vfx_pattern02.nif -Decoding meshes\e\vfx_pattern04.nif -Decoding meshes\e\vfx_pattern03.nif -Decoding meshes\e\vfx_pattern05.nif -Decoding meshes\e\vfx_pattern08.nif -Decoding meshes\m\repair_master_01.nif -Decoding meshes\i\active_port_hlor.nif -Decoding meshes\i\active_port_indo.nif -Decoding meshes\i\active_port_roth.nif -Decoding meshes\r\xcorprus_stalker.nif -Decoding meshes\r\xbonewalker.nif -Decoding meshes\xbase_anim_female.nif -Decoding meshes\xbase_animkna.1st.nif -Decoding meshes\xbase_animkna.nif -Decoding meshes\xbase_anim.1st.nif -Decoding meshes\a\a_m_imperialchain_pauldron_gnd.nif -Decoding meshes\a\a_m_imperialchain_greaves_gnd.nif -Decoding meshes\a\a_m_imperialchain_pauldron_ua.nif -Decoding meshes\a\a_m_imperialchain_greaves_g.nif -Decoding meshes\a\a_m_imperialchain_greaves_ul.nif -Decoding meshes\w\w_iron_shortsword.nif -Decoding meshes\m\misc_foldedcloth00.nif -Decoding meshes\m\misc_de_tankard_01.nif -Decoding meshes\m\misc_de_goblet_09.nif -Decoding meshes\m\misc_de_goblet_08.nif -Decoding meshes\m\misc_de_goblet_01.nif -Decoding meshes\m\misc_de_goblet_03.nif -Decoding meshes\m\misc_de_goblet_02.nif -Decoding meshes\m\misc_de_goblet_05.nif -Decoding meshes\m\misc_de_goblet_04.nif -Decoding meshes\m\misc_de_goblet_07.nif -Decoding meshes\m\misc_de_goblet_06.nif -Decoding meshes\m\misc_dwrv_goblet00.nif -Decoding meshes\m\misc_dwrv_goblet10.nif -Decoding meshes\m\misc_de_pitcher_01.nif -Decoding meshes\m\misc_de_basket_01.nif -Decoding meshes\m\misc_bowl_white_01.nif -Decoding meshes\m\misc_com_bucket_01.nif -Decoding meshes\m\misc_com_pillow_01.nif -Decoding meshes\m\misc_com_basket_02.nif -Decoding meshes\m\misc_com_basket_01.nif -Decoding meshes\m\misc_com_bottle_06.nif -Decoding meshes\m\misc_com_bottle_07.nif -Decoding meshes\m\misc_com_bottle_14.nif -Decoding meshes\m\misc_com_bottle_04.nif -Decoding meshes\m\misc_com_bottle_15.nif -Decoding meshes\m\misc_com_bottle_05.nif -Decoding meshes\m\misc_com_bottle_12.nif -Decoding meshes\m\misc_com_bottle_02.nif -Decoding meshes\m\misc_com_bottle_13.nif -Decoding meshes\m\misc_com_bottle_03.nif -Decoding meshes\m\misc_com_bottle_10.nif -Decoding meshes\m\misc_com_bottle_11.nif -Decoding meshes\m\misc_com_bottle_01.nif -Decoding meshes\m\misc_com_bottle_08.nif -Decoding meshes\m\misc_com_bottle_09.nif -Decoding meshes\m\misc_com_wood_fork.nif -Decoding meshes\m\misc_com_broom_01.nif -Decoding meshes\m\misc_com_plate_06.nif -Decoding meshes\m\misc_com_plate_07.nif -Decoding meshes\m\misc_com_plate_04.nif -Decoding meshes\m\misc_com_plate_05.nif -Decoding meshes\m\misc_com_plate_02.nif -Decoding meshes\m\misc_com_plate_03.nif -Decoding meshes\m\misc_com_plate_01.nif -Decoding meshes\m\misc_com_plate_08.nif -Decoding meshes\m\misc_wheatbundle00.nif -Decoding meshes\m\misc_uni_pillow_02.nif -Decoding meshes\m\misc_redware_bowl.nif -Decoding meshes\m\misc_redware_flask.nif -Decoding meshes\m\misc_rollingpin_01.nif -Decoding meshes\m\misc_redware_vase.nif -Decoding meshes\m\misc_redware_plate.nif -Decoding meshes\m\misc_redware_lamp.nif -Decoding meshes\m\misc_soulgem_grand.nif -Decoding meshes\m\misc_soulgem_petty.nif -Decoding meshes\m\misc_pot_green_01.nif -Decoding meshes\m\misc_portal_shard.nif -Decoding meshes\f\xex_ashl_z_banner.nif -Decoding meshes\f\xex_ashl_a_banner.nif -Decoding meshes\f\xex_ashl_u_banner.nif -Decoding meshes\f\xex_ashl_e_banner.nif -Decoding meshes\l\furn_de_firepit_f.nif -Decoding meshes\r\golden saint.nif -Decoding meshes\m\misc_scrapwood01.nif -Decoding meshes\m\misc_scrapwood03.nif -Decoding meshes\m\misc_scrapwood02.nif -Decoding meshes\m\misc_scrapwood05.nif -Decoding meshes\m\misc_scrapwood04.nif -Decoding meshes\m\misc_lw_platter.nif -Decoding meshes\m\misc_dwrv_bowl00.nif -Decoding meshes\m\misc_de_drum_02.nif -Decoding meshes\m\misc_shackles00.nif -Decoding meshes\m\misc_redware_cup.nif -Decoding meshes\m\misc_ropecoil00.nif -Decoding meshes\m\misc_wickwheat00.nif -Decoding meshes\m\misc_de_bowl_01.nif -Decoding meshes\m\misc_pot_blue_01.nif -Decoding meshes\m\misc_pot_blue_02.nif -Decoding meshes\m\misc_de_lute_01.nif -Decoding meshes\m\misc_dwrv_coin00.nif -Decoding meshes\m\misc_de_drum_01.nif -Decoding meshes\m\misc_6th_goblet.nif -Decoding meshes\m\misc_dwrv_gear00.nif -Decoding meshes\m\misc_placemat_01.nif -Decoding meshes\m\misc_dwrv_mug00.nif -Decoding meshes\r\siltstrider.nif -Decoding meshes\m\misc_flask_02.nif -Decoding meshes\m\misc_hammer10.nif -Decoding meshes\m\misc_prongs00.nif -Decoding meshes\m\misc_shears_01.nif -Decoding meshes\m\misc_beaker_01.nif -Decoding meshes\m\misc_spool_01.nif -Decoding meshes\m\misc_flask_01.nif -Decoding meshes\m\misc_bellows10.nif -Decoding meshes\m\misc_flask_04.nif -Decoding meshes\m\misc_lw_flask.nif -Decoding meshes\m\misc_flask_03.nif -Decoding meshes\m\misc_lw_cup.nif -Decoding meshes\m\misc_cloth10.nif -Decoding meshes\m\misc_lw_bowl.nif -Decoding meshes\m\misc_inkwell.nif -Decoding meshes\m\misc_cloth11.nif -Decoding meshes\m\misc_skull00.nif -Decoding meshes\m\misc_skull10.nif -Decoding meshes\r\xclannfear_daddy.nif -Decoding meshes\r\xcliffracer.nif -Decoding meshes\steam_bluegreen.nif -Decoding meshes\steam_lavariver.nif -Decoding meshes\l\furn_de_chair_02.nif -Decoding meshes\r\xslaughterfish.nif -Decoding meshes\w\w_iron_claymore.nif -Decoding meshes\w\w_iron_longsword.nif -Decoding meshes\w\w_iron_dagger.nif -Decoding meshes\w\w_iron_arrow.nif -Decoding meshes\sky_moon_small.nif -Decoding meshes\sky_moon_large.nif -Decoding meshes\l\misc_candle_red_01.nif -Decoding meshes\r\lame_corprus.nif -Decoding meshes\w\w_halberd_glass.nif -Decoding meshes\w\w_halberd_steel.nif -Decoding meshes\w\w_halberd_iron.nif -Decoding meshes\c\c_shirt_aralor_fa.nif -Decoding meshes\c\c_shirt_aralor_ua.nif -Decoding meshes\c\c_shirt_aralor_gnd.nif -Decoding meshes\sky_clouds_01_no_tex.nif -Decoding meshes\w\magic_target_myst.nif -Decoding meshes\w\magic_target_rest.nif -Decoding meshes\w\magic_target_frost.nif -Decoding meshes\c\c_shoes_extrav_2_f.nif -Decoding meshes\c\c_shoes_extrav_1_f.nif -Decoding meshes\c\c_shoes_rilms_gnd.nif -Decoding meshes\e\soultraphit.nif -Decoding meshes\w\magic_target_ill.nif -Decoding meshes\w\magic_target_dst.nif -Decoding meshes\w\magic_target_alt.nif -Decoding meshes\w\magic_target.nif -Decoding meshes\c\c_shoes_common_3.nif -Decoding meshes\c\c_shoes_common_4.nif -Decoding meshes\c\c_shoes_common_5.nif -Decoding meshes\c\c_shirt_aralor_c.nif -Decoding meshes\c\c_shirt_aralor_w.nif -Decoding meshes\c\c_skirt_common_5.nif -Decoding meshes\c\c_skirt_common_2.nif -Decoding meshes\c\c_skirt_common_3.nif -Decoding meshes\w\w_glass_arrow.nif -Decoding meshes\sky_clouds_01.nif -Decoding meshes\c\c_slave_bracer.nif -Decoding meshes\c\c_shoes_rilms.nif -Decoding meshes\r\g_centurionspider.nif -Decoding meshes\c\c_ring_exquisite_1.nif -Decoding meshes\c\c_ring_expensive_1.nif -Decoding meshes\c\c_ring_expensive_3.nif -Decoding meshes\c\c_ring_expensive_2.nif -Decoding meshes\r\ancestorghost.nif -Decoding meshes\r\ascendedsleeper.nif -Decoding meshes\r\xscamp_fetch.nif -Decoding meshes\c\c_ring_common05.nif -Decoding meshes\c\c_ring_common01.nif -Decoding meshes\c\c_ring_common03.nif -Decoding meshes\c\c_ring_moonnstar.nif -Decoding meshes\c\c_ring_common04.nif -Decoding meshes\c\c_ring_common02.nif -Decoding meshes\c\c_ring_namira.nif -Decoding meshes\a\a_silver_duke_gnd.nif -Decoding meshes\a\a_silver_cuir_gnd.nif -Decoding meshes\a\a_steel_greaves_ul.nif -Decoding meshes\a\a_steel_greaves_g.nif -Decoding meshes\a\a_steel_greaves_k.nif -Decoding meshes\a\a_steel_hands.1st.nif -Decoding meshes\a\a_steel_boots_gnd.nif -Decoding meshes\a\a_shield_imperial.nif -Decoding meshes\f\active_de_bar_door.nif -Decoding meshes\f\active_akhul_steam.nif -Decoding meshes\f\active_signpost_01.nif -Decoding meshes\f\active_signpost_02.nif -Decoding meshes\f\active_de_bedroll.nif -Decoding meshes\f\act_banner_hla_oad.nif -Decoding meshes\f\act_banner_tel_fyr.nif -Decoding meshes\f\act_banner_tel_vos.nif -Decoding meshes\f\active_de_bed_19.nif -Decoding meshes\f\active_de_bed_18.nif -Decoding meshes\f\active_de_bed_13.nif -Decoding meshes\f\active_de_bed_12.nif -Decoding meshes\f\active_de_bed_11.nif -Decoding meshes\f\active_de_bed_10.nif -Decoding meshes\f\active_de_bed_17.nif -Decoding meshes\f\active_de_bed_16.nif -Decoding meshes\f\active_de_bed_15.nif -Decoding meshes\f\active_de_bed_14.nif -Decoding meshes\f\active_de_bed_09.nif -Decoding meshes\f\active_de_bed_08.nif -Decoding meshes\f\active_de_bed_03.nif -Decoding meshes\f\active_de_bed_02.nif -Decoding meshes\f\active_de_bed_01.nif -Decoding meshes\f\active_de_bed_07.nif -Decoding meshes\f\active_de_bed_06.nif -Decoding meshes\f\active_de_bed_05.nif -Decoding meshes\f\active_de_bed_04.nif -Decoding meshes\f\active_de_bed_30.nif -Decoding meshes\f\active_de_bed_29.nif -Decoding meshes\f\active_de_bed_28.nif -Decoding meshes\f\active_de_bed_23.nif -Decoding meshes\f\active_de_bed_22.nif -Decoding meshes\f\active_de_bed_21.nif -Decoding meshes\f\active_de_bed_20.nif -Decoding meshes\f\active_de_bed_27.nif -Decoding meshes\f\active_de_bed_26.nif -Decoding meshes\f\active_de_bed_25.nif -Decoding meshes\f\active_de_bed_24.nif -Decoding meshes\f\active_bubbles00.nif -Decoding meshes\f\active_button_01.nif -Decoding meshes\f\act_banner_khull.nif -Decoding meshes\f\act_banner_vos.nif -Decoding meshes\f\active_gong_01.nif -Decoding meshes\a\a_silver_cuirass.nif -Decoding meshes\w\w_ebony_arrow.nif -Decoding meshes\vfx_defaultcast.nif -Decoding meshes\vfx_defaultarea.nif -Decoding meshes\vfx_defaulthit.nif -Decoding meshes\sky_atmosphere.nif -Decoding meshes\a\a_steel_helmet.nif -Decoding meshes\a\a_steel_boot_f.nif -Decoding meshes\a\a_steel_boot_a.nif -Decoding meshes\a\a_steel_skin.nif -Decoding meshes\f\xfurn_imp_flag_01.nif -Decoding meshes\a\a_ringmail_cuirass.nif -Decoding meshes\w\w_dwemer_warhammer.nif -Decoding meshes\w\w_dwemer_claymore.nif -Decoding meshes\w\w_dwemer_battleaxe.nif -Decoding meshes\w\w_dwemer_longspear.nif -Decoding meshes\a\a_redoranmaster_h.nif -Decoding meshes\f\sound_dummy00.nif -Decoding meshes\r\clannfear_daddy.nif -Decoding meshes\r\slaughterfish.nif -Decoding meshes\r\xlame_corprus.nif -Decoding meshes\r\heart_akulakhan.nif -Decoding meshes\r\leastkagouti.nif -Decoding meshes\r\scamp_fetch.nif -Decoding meshes\r\xcavemudcrab.nif -Decoding meshes\r\dwarvenspecter.nif -Decoding meshes\r\kwama forager.nif -Decoding meshes\r\kwama worker.nif -Decoding meshes\r\kwama queen.nif -Decoding meshes\r\kwama warior.nif -Decoding meshes\r\guar_withpack.nif -Decoding meshes\w\w_dwemer_waraxe.nif -Decoding meshes\w\w_dwemer_halberd.nif -Decoding meshes\w\w_daedric_arrow.nif -Decoding meshes\w\w_dagger_dragon.nif -Decoding meshes\w\w_dagger_daedric.nif -Decoding meshes\w\w_dagger_chitin.nif -Decoding meshes\w\w_dwemer_spear.nif -Decoding meshes\w\w_dwemer_mace.nif -Decoding meshes\w\w_dreugh_staff.nif -Decoding meshes\w\w_dreugh_club.nif -Decoding meshes\w\w_dagger_glass.nif -Decoding meshes\w\w_dart_silver.nif -Decoding meshes\w\w_dart_daedric.nif -Decoding meshes\w\w_dart_ebony.nif -Decoding meshes\w\w_dart_steel.nif -Decoding meshes\w\w_daikatana.nif -Decoding meshes\b\b_v_orc_m_head_01.nif -Decoding meshes\b\b_v_orc_f_head_01.nif -Decoding meshes\w\w_crossbow_dwemer.nif -Decoding meshes\w\w_claymore_crystal.nif -Decoding meshes\w\w_claymore_daedric.nif -Decoding meshes\b\b_v_nord_f_head_01.nif -Decoding meshes\b\b_v_nord_m_head_01.nif -Decoding meshes\f\terrain_bc_scum_01.nif -Decoding meshes\f\terrain_bc_scum_03.nif -Decoding meshes\f\terrain_bc_scum_02.nif -Decoding meshes\f\terrain_rock_ac_08.nif -Decoding meshes\f\terrain_rock_bc_08.nif -Decoding meshes\f\terrain_rock_bc_18.nif -Decoding meshes\f\terrain_rock_ac_09.nif -Decoding meshes\f\terrain_rock_bc_09.nif -Decoding meshes\f\terrain_rock_ac_02.nif -Decoding meshes\f\terrain_rock_ac_12.nif -Decoding meshes\f\terrain_rock_bc_02.nif -Decoding meshes\f\terrain_rock_bc_12.nif -Decoding meshes\f\terrain_rock_ac_03.nif -Decoding meshes\f\terrain_rock_bc_03.nif -Decoding meshes\f\terrain_rock_bc_13.nif -Decoding meshes\f\terrain_rock_ac_10.nif -Decoding meshes\f\terrain_rock_bc_10.nif -Decoding meshes\f\terrain_rock_ac_01.nif -Decoding meshes\f\terrain_rock_ac_11.nif -Decoding meshes\f\terrain_rock_bc_01.nif -Decoding meshes\f\terrain_rock_bc_11.nif -Decoding meshes\f\terrain_rock_ac_06.nif -Decoding meshes\f\terrain_rock_bc_06.nif -Decoding meshes\f\terrain_rock_bc_16.nif -Decoding meshes\f\terrain_rock_ac_07.nif -Decoding meshes\f\terrain_rock_bc_07.nif -Decoding meshes\f\terrain_rock_bc_17.nif -Decoding meshes\f\terrain_rock_ac_04.nif -Decoding meshes\f\terrain_rock_bc_04.nif -Decoding meshes\f\terrain_rock_bc_14.nif -Decoding meshes\f\terrain_rock_ac_05.nif -Decoding meshes\f\terrain_rock_bc_05.nif -Decoding meshes\f\terrain_rock_bc_15.nif -Decoding meshes\f\terrain_rock_ai_09.nif -Decoding meshes\f\terrain_rock_ai_08.nif -Decoding meshes\f\terrain_rock_ai_01.nif -Decoding meshes\f\terrain_rock_ai_11.nif -Decoding meshes\f\terrain_rock_ai_10.nif -Decoding meshes\f\terrain_rock_ai_03.nif -Decoding meshes\f\terrain_rock_ai_02.nif -Decoding meshes\f\terrain_rock_ai_12.nif -Decoding meshes\f\terrain_rock_ai_05.nif -Decoding meshes\f\terrain_rock_ai_04.nif -Decoding meshes\f\terrain_rock_ai_07.nif -Decoding meshes\f\terrain_rock_ai_06.nif -Decoding meshes\f\terrain_rock_wg_09.nif -Decoding meshes\f\terrain_rock_wg_18.nif -Decoding meshes\f\terrain_rock_wg_08.nif -Decoding meshes\f\terrain_rock_wg_15.nif -Decoding meshes\f\terrain_rock_wg_05.nif -Decoding meshes\f\terrain_rock_wg_14.nif -Decoding meshes\f\terrain_rock_wg_04.nif -Decoding meshes\f\terrain_rock_wg_17.nif -Decoding meshes\f\terrain_rock_wg_07.nif -Decoding meshes\f\terrain_rock_wg_16.nif -Decoding meshes\f\terrain_rock_wg_06.nif -Decoding meshes\f\terrain_rock_wg_11.nif -Decoding meshes\f\terrain_rock_wg_01.nif -Decoding meshes\f\terrain_rock_wg_10.nif -Decoding meshes\f\terrain_rock_wg_13.nif -Decoding meshes\f\terrain_rock_wg_03.nif -Decoding meshes\f\terrain_rock_wg_12.nif -Decoding meshes\f\terrain_rock_wg_02.nif -Decoding meshes\f\terrain_rock_ma_01.nif -Decoding meshes\f\terrain_boulder_02.nif -Decoding meshes\f\terrain_boulder_03.nif -Decoding meshes\f\terrain_boulder_01.nif -Decoding meshes\f\terrain_boulder_04.nif -Decoding meshes\f\terrain_boulder_05.nif -Decoding meshes\f\terrain_rock_gl_03.nif -Decoding meshes\f\terrain_rock_gl_02.nif -Decoding meshes\f\terrain_rock_gl_12.nif -Decoding meshes\f\terrain_rock_gl_01.nif -Decoding meshes\f\terrain_rock_gl_11.nif -Decoding meshes\f\terrain_rock_gl_10.nif -Decoding meshes\f\terrain_rock_gl_07.nif -Decoding meshes\f\terrain_rock_gl_06.nif -Decoding meshes\f\terrain_rock_gl_05.nif -Decoding meshes\f\terrain_rock_gl_04.nif -Decoding meshes\f\terrain_rock_gl_09.nif -Decoding meshes\f\terrain_rock_gl_08.nif -Decoding meshes\f\terrain_rock_rm_09.nif -Decoding meshes\f\terrain_rock_rm_19.nif -Decoding meshes\f\terrain_rock_rm_08.nif -Decoding meshes\f\terrain_rock_rm_18.nif -Decoding meshes\f\terrain_rock_rm_07.nif -Decoding meshes\f\terrain_rock_rm_17.nif -Decoding meshes\f\terrain_rock_rm_06.nif -Decoding meshes\f\terrain_rock_rm_16.nif -Decoding meshes\f\terrain_rock_rm_05.nif -Decoding meshes\f\terrain_rock_rm_15.nif -Decoding meshes\f\terrain_rock_rm_04.nif -Decoding meshes\f\terrain_rock_rm_14.nif -Decoding meshes\f\terrain_rock_rm_24.nif -Decoding meshes\f\terrain_rock_rm_03.nif -Decoding meshes\f\terrain_rock_rm_13.nif -Decoding meshes\f\terrain_rock_rm_23.nif -Decoding meshes\f\terrain_rock_rm_02.nif -Decoding meshes\f\terrain_rock_rm_12.nif -Decoding meshes\f\terrain_rock_rm_22.nif -Decoding meshes\f\terrain_rock_rm_01.nif -Decoding meshes\f\terrain_rock_rm_11.nif -Decoding meshes\f\terrain_rock_rm_21.nif -Decoding meshes\f\terrain_rock_rm_10.nif -Decoding meshes\f\terrain_rock_rm_20.nif -Decoding meshes\f\furn_roped_pole_01.nif -Decoding meshes\f\furn_rail_broke00.nif -Decoding meshes\f\furn_rail_elbow_00.nif -Decoding meshes\f\furn_rail_slope_00.nif -Decoding meshes\f\furn_stickbundle00.nif -Decoding meshes\f\furn_smokestack00.nif -Decoding meshes\f\furn_pathspear_03.nif -Decoding meshes\f\furn_pathspear_02.nif -Decoding meshes\f\furn_pathspear_01.nif -Decoding meshes\f\furn_pathspear_04.nif -Decoding meshes\f\furn_pycave_pool00.nif -Decoding meshes\f\furn_wallscreen_01.nif -Decoding meshes\f\furn_wallscreen_02.nif -Decoding meshes\f\furn_triolith_01a.nif -Decoding meshes\f\furn_halfbarrel01.nif -Decoding meshes\f\furn_halfbarrel00.nif -Decoding meshes\f\furn_imp_altar_01.nif -Decoding meshes\f\furn_imp_metalring.nif -Decoding meshes\f\dwrv_mechlfrarm00.nif -Decoding meshes\f\dwrv_mechrthigh00.nif -Decoding meshes\f\dwrv_mechlthigh00.nif -Decoding meshes\f\furn_bone_stake00.nif -Decoding meshes\f\furn_bone_skull_01.nif -Decoding meshes\f\furn_bannerpost_02.nif -Decoding meshes\f\furn_bannerpost_01.nif -Decoding meshes\f\furn_c_t_shadow_01.nif -Decoding meshes\f\furn_c_t_theif_01.nif -Decoding meshes\f\furn_com_bar_door.nif -Decoding meshes\f\furn_c_t_tower_01.nif -Decoding meshes\f\furn_c_t_arkay_01.nif -Decoding meshes\f\furn_com_kegstand.nif -Decoding meshes\f\furn_clothbolt_02.nif -Decoding meshes\f\furn_clothbolt_03.nif -Decoding meshes\f\furn_clothbolt_01.nif -Decoding meshes\f\furn_com_table_01.nif -Decoding meshes\f\furn_com_table_03.nif -Decoding meshes\f\furn_com_table_02.nif -Decoding meshes\f\furn_com_table_05.nif -Decoding meshes\f\furn_com_table_04.nif -Decoding meshes\f\furn_com_stool_01.nif -Decoding meshes\f\furn_com_stool_02.nif -Decoding meshes\f\furn_com_barstool.nif -Decoding meshes\f\furn_c_t_ritual_01.nif -Decoding meshes\f\furn_c_t_wizard_01.nif -Decoding meshes\f\furn_c_t_lover_01.nif -Decoding meshes\f\furn_crate_lid_01.nif -Decoding meshes\f\furn_com_chair_01.nif -Decoding meshes\f\furn_com_chair_03.nif -Decoding meshes\f\furn_com_chair_02.nif -Decoding meshes\f\furn_com_winerack.nif -Decoding meshes\f\furn_c_t_steed_01.nif -Decoding meshes\f\furn_c_t_golem_01.nif -Decoding meshes\f\furn_com_shelf_04.nif -Decoding meshes\f\furn_com_shelf_03.nif -Decoding meshes\f\furn_com_shelf_02.nif -Decoding meshes\f\furn_com_shelf_01.nif -Decoding meshes\f\furn_crate_open_04.nif -Decoding meshes\f\furn_crate_open_05.nif -Decoding meshes\f\furn_crate_open_01.nif -Decoding meshes\f\furn_com_bench_02.nif -Decoding meshes\f\furn_com_bench_01.nif -Decoding meshes\f\furn_ashl_bugbowl.nif -Decoding meshes\f\furn_ashl_chime_03.nif -Decoding meshes\f\furn_ashl_chime_02.nif -Decoding meshes\f\furn_ashl_chime_01.nif -Decoding meshes\f\furn_ashl_chime_07.nif -Decoding meshes\f\furn_ashl_chime_06.nif -Decoding meshes\f\furn_ashl_chime_05.nif -Decoding meshes\f\furn_ashl_chime_04.nif -Decoding meshes\f\furn_ashl_chime_08.nif -Decoding meshes\f\furn_dwrv_bench10.nif -Decoding meshes\f\furn_dwrv_bench00.nif -Decoding meshes\f\furn_dae_rubble_07.nif -Decoding meshes\f\furn_dae_rubble_06.nif -Decoding meshes\f\furn_dae_rubble_05.nif -Decoding meshes\f\furn_de_ashl_post.nif -Decoding meshes\f\furn_de_shack_post.nif -Decoding meshes\f\furn_dwrv_steam_00.nif -Decoding meshes\f\furn_dwrv_dynamo00.nif -Decoding meshes\f\furn_de_bellows_01.nif -Decoding meshes\f\furn_dwrv_table10.nif -Decoding meshes\f\furn_dwrv_table00.nif -Decoding meshes\f\furn_de_firepit_01.nif -Decoding meshes\f\furn_dwrv_bucket00.nif -Decoding meshes\f\furn_dwrv_table20.nif -Decoding meshes\f\furn_de_shack_hook.nif -Decoding meshes\f\furn_dwrv_chair00.nif -Decoding meshes\f\furn_dwrv_stove00.nif -Decoding meshes\f\furn_dwrv_stove10.nif -Decoding meshes\f\furn_dwrv_tranny01.nif -Decoding meshes\f\furn_dwrv_tranny00.nif -Decoding meshes\f\furn_dwrv_stool00.nif -Decoding meshes\f\furn_dwrv_stool10.nif -Decoding meshes\x\collision01.nif -Decoding meshes\m\key_standard_01.nif -Decoding meshes\m\key_temple_01.nif -Decoding meshes\f\dwrv_mechrarm00.nif -Decoding meshes\f\dwrv_mechhead00.nif -Decoding meshes\f\dwrv_mechlhand00.nif -Decoding meshes\f\dwrv_mechlfoot00.nif -Decoding meshes\f\dwrv_mechrfoot00.nif -Decoding meshes\f\dwrv_mechrfarm00.nif -Decoding meshes\f\dwrv_mechhips00.nif -Decoding meshes\f\dwrv_mechrhand00.nif -Decoding meshes\f\dwrv_mechtorso00.nif -Decoding meshes\f\dwrv_mechlarm00.nif -Decoding meshes\f\furn_c_t_lord_01.nif -Decoding meshes\f\furn_de_winerack.nif -Decoding meshes\f\furn_de_shelf_02.nif -Decoding meshes\f\furn_de_shelf_01.nif -Decoding meshes\f\furn_overhang_09.nif -Decoding meshes\f\furn_overhang_01.nif -Decoding meshes\f\furn_overhang_03.nif -Decoding meshes\f\furn_overhang_02.nif -Decoding meshes\f\furn_overhang_05.nif -Decoding meshes\f\furn_overhang_04.nif -Decoding meshes\f\furn_overhang_07.nif -Decoding meshes\f\furn_overhang_06.nif -Decoding meshes\f\furn_spinwheel00.nif -Decoding meshes\f\furn_overhang_18.nif -Decoding meshes\f\furn_c_t_mara_01.nif -Decoding meshes\f\furn_6th_banner.nif -Decoding meshes\f\furn_de_chair_01.nif -Decoding meshes\f\furn_de_chair_02.nif -Decoding meshes\f\furn_de_chair_03.nif -Decoding meshes\f\furn_de_forge_01.nif -Decoding meshes\f\furn_pottedplant.nif -Decoding meshes\f\furn_triolith_01.nif -Decoding meshes\f\furn_coalpile00.nif -Decoding meshes\f\furn_de_table_07.nif -Decoding meshes\f\furn_de_table_06.nif -Decoding meshes\f\furn_de_table_05.nif -Decoding meshes\f\furn_de_table_04.nif -Decoding meshes\f\furn_de_table_03.nif -Decoding meshes\f\furn_de_table_02.nif -Decoding meshes\f\furn_de_table_01.nif -Decoding meshes\f\furn_de_table_09.nif -Decoding meshes\f\furn_de_table_08.nif -Decoding meshes\f\furn_cistern_01.nif -Decoding meshes\f\furn_com_bar_02.nif -Decoding meshes\f\furn_com_bar_04.nif -Decoding meshes\f\furn_com_bar_06.nif -Decoding meshes\f\furn_tapestry10.nif -Decoding meshes\f\furn_rug_big_08.nif -Decoding meshes\f\furn_rug_big_02.nif -Decoding meshes\f\furn_rug_big_04.nif -Decoding meshes\f\furn_rug_big_06.nif -Decoding meshes\f\furn_cot_rug_02.nif -Decoding meshes\f\furn_de_bench_03.nif -Decoding meshes\f\furn_de_bench_02.nif -Decoding meshes\f\furn_de_bench_01.nif -Decoding meshes\f\furn_de_bench_04.nif -Decoding meshes\f\furn_com_bed_02.nif -Decoding meshes\f\furn_com_bed_04.nif -Decoding meshes\f\furn_com_bed_06.nif -Decoding meshes\f\furn_de_loom_01.nif -Decoding meshes\f\furn_planter_01.nif -Decoding meshes\f\furn_planter_03.nif -Decoding meshes\f\furn_de_rope_05.nif -Decoding meshes\f\furn_de_rope_07.nif -Decoding meshes\f\furn_de_rope_03.nif -Decoding meshes\f\furn_c_t_lady_01.nif -Decoding meshes\f\furn_dwrv_bed00.nif -Decoding meshes\f\furn_rail_end00.nif -Decoding meshes\f\furn_woodbar_01.nif -Decoding meshes\f\furn_de_kegstand.nif -Decoding meshes\f\furn_woodpost_01.nif -Decoding meshes\f\furn_woodpole_01.nif -Decoding meshes\f\furn_tapestry00.nif -Decoding meshes\f\furn_tapestry20.nif -Decoding meshes\f\furn_de_stool_01.nif -Decoding meshes\f\furn_de_stool_02.nif -Decoding meshes\f\furn_de_firepit.nif -Decoding meshes\f\furn_bone_rib_01.nif -Decoding meshes\f\furn_bed_rug_01.nif -Decoding meshes\f\furn_imp_flag_01.nif -Decoding meshes\f\furn_com_bunk_02.nif -Decoding meshes\f\furn_com_bunk_01.nif -Decoding meshes\f\furn_de_lecturn.nif -Decoding meshes\f\furn_guarcart00.nif -Decoding meshes\f\furn_com_planter.nif -Decoding meshes\f\furn_fireplace10.nif -Decoding meshes\f\furn_signbase_02.nif -Decoding meshes\f\furn_com_bar_01.nif -Decoding meshes\f\furn_com_bar_03.nif -Decoding meshes\f\furn_com_bar_05.nif -Decoding meshes\f\furn_tapestry30.nif -Decoding meshes\f\furn_dwrv_well00.nif -Decoding meshes\f\furn_rug_big_09.nif -Decoding meshes\f\furn_rug_big_01.nif -Decoding meshes\f\furn_rug_big_03.nif -Decoding meshes\f\furn_rug_big_05.nif -Decoding meshes\f\furn_rug_big_07.nif -Decoding meshes\f\furn_cot_rug_01.nif -Decoding meshes\f\furn_cot_rug_03.nif -Decoding meshes\f\furn_com_bed_01.nif -Decoding meshes\f\furn_com_bed_03.nif -Decoding meshes\f\furn_com_bed_05.nif -Decoding meshes\f\furn_com_bed_07.nif -Decoding meshes\f\furn_planter_02.nif -Decoding meshes\f\furn_planter_04.nif -Decoding meshes\f\furn_de_rope_04.nif -Decoding meshes\f\furn_de_rope_06.nif -Decoding meshes\f\furn_netramp_01.nif -Decoding meshes\f\furn_rope1_01.nif -Decoding meshes\f\furn_rope2_01.nif -Decoding meshes\f\furn_winekeg00.nif -Decoding meshes\f\furn_bedmat_01.nif -Decoding meshes\f\furn_basin_01.nif -Decoding meshes\f\furn_basket_01.nif -Decoding meshes\f\furn_logpile10.nif -Decoding meshes\f\furn_6th_bell2.nif -Decoding meshes\f\furn_6th_bell4.nif -Decoding meshes\f\furn_6th_bell6.nif -Decoding meshes\f\furn_de_bar_02.nif -Decoding meshes\f\furn_de_bar_04.nif -Decoding meshes\f\furn_de_bar_06.nif -Decoding meshes\f\furn_stool_01.nif -Decoding meshes\f\furn_com_de_01.nif -Decoding meshes\f\furn_ashpit_02.nif -Decoding meshes\f\furn_skull_01.nif -Decoding meshes\f\furn_grill_01.nif -Decoding meshes\f\furn_table_01.nif -Decoding meshes\f\furn_chair_02.nif -Decoding meshes\f\furn_pillow_01.nif -Decoding meshes\f\furn_sconce_01.nif -Decoding meshes\f\furn_banner_01.nif -Decoding meshes\f\furn_cabinet10.nif -Decoding meshes\f\furn_firepit00.nif -Decoding meshes\f\furn_burial10.nif -Decoding meshes\f\furn_burial00.nif -Decoding meshes\f\furn_burial20.nif -Decoding meshes\f\furn_6th_bells.nif -Decoding meshes\f\furn_6th_bell1.nif -Decoding meshes\f\furn_6th_bell3.nif -Decoding meshes\f\furn_6th_bell5.nif -Decoding meshes\f\furn_de_bar_01.nif -Decoding meshes\f\furn_de_bar_03.nif -Decoding meshes\f\furn_de_bar_05.nif -Decoding meshes\f\furn_ashpit_01.nif -Decoding meshes\f\furn_bucket10.nif -Decoding meshes\f\furn_shell10.nif -Decoding meshes\f\furn_shell00.nif -Decoding meshes\f\furn_shell20.nif -Decoding meshes\f\furn_mist512.nif -Decoding meshes\f\furn_rug_04.nif -Decoding meshes\f\furn_rug_01.nif -Decoding meshes\f\furn_rug_03.nif -Decoding meshes\f\furn_rug_02.nif -Decoding meshes\f\furn_torch00.nif -Decoding meshes\f\furn_bone_01.nif -Decoding meshes\f\furn_mist256.nif -Decoding meshes\f\furn_hook_01.nif -Decoding meshes\f\furn_log_04.nif -Decoding meshes\f\furn_log_01.nif -Decoding meshes\f\furn_log_03.nif -Decoding meshes\f\furn_log_02.nif -Decoding meshes\f\furn_table10.nif -Decoding meshes\f\furn_cart00.nif -Decoding meshes\f\furn_anvil00.nif -Decoding meshes\f\furn_well00.nif -Decoding meshes\f\furn_tray_01.nif -Decoding meshes\f\furn_6th_troth_01.nif -Decoding meshes\f\furn_6th_troth_02.nif -Decoding meshes\f\furn_6th_ashaltar.nif -Decoding meshes\f\furn_6th_ashstatue.nif -Decoding meshes\f\furn_6th_ashpillar.nif -Decoding meshes\f\furn_6th_platform.nif -Decoding meshes\w\w_crossbow_steel.nif -Decoding meshes\w\w_corkbulb_arrow.nif -Decoding meshes\w\w_club_daedric.nif -Decoding meshes\w\w_chitin_arrow.nif -Decoding meshes\w\w_chitin_star.nif -Decoding meshes\w\w_chitin_club.nif -Decoding meshes\w\w_chitin_spear.nif -Decoding meshes\w\w_chitin_axe.nif -Decoding meshes\w\w_club_iron.nif -Decoding meshes\a\a_tenpaceboot_gnd.nif -Decoding meshes\a\a_templar_m_f_boot.nif -Decoding meshes\a\a_templar_m_a_boot.nif -Decoding meshes\a\a_templar_m_helmet.nif -Decoding meshes\a\a_templar_w_bracer.nif -Decoding meshes\a\a_templar_m_skins.nif -Decoding meshes\w\w_broadsword_iron.nif -Decoding meshes\w\w_broadsword_ebony.nif -Decoding meshes\m\text_folio_open_04.nif -Decoding meshes\m\text_folio_open_01.nif -Decoding meshes\m\text_folio_open_02.nif -Decoding meshes\m\text_folio_open_03.nif -Decoding meshes\m\text_paper_roll_01.nif -Decoding meshes\m\text_parchment_02.nif -Decoding meshes\m\text_parchment_01.nif -Decoding meshes\xanim_dancinggirl.nif -Decoding meshes\m\text_octavo_05.nif -Decoding meshes\m\text_octavo_07.nif -Decoding meshes\m\text_octavo_01.nif -Decoding meshes\m\text_octavo_03.nif -Decoding meshes\m\text_quarto_03.nif -Decoding meshes\m\text_quarto_01.nif -Decoding meshes\m\text_folio_01.nif -Decoding meshes\m\text_scroll_02.nif -Decoding meshes\m\text_folio_04.nif -Decoding meshes\m\text_folio_03.nif -Decoding meshes\m\text_folio_02.nif -Decoding meshes\m\text_octavo_04.nif -Decoding meshes\m\text_octavo_06.nif -Decoding meshes\m\text_octavo_02.nif -Decoding meshes\m\text_octavo_08.nif -Decoding meshes\m\text_quarto_02.nif -Decoding meshes\m\text_quarto_04.nif -Decoding meshes\m\text_scroll_01.nif -Decoding meshes\m\text_scroll_03.nif -Decoding meshes\m\text_note_02.nif -Decoding meshes\m\text_note_01.nif -Decoding meshes\w\w_bolt_bonemold.nif -Decoding meshes\w\w_bolt_corkbulb.nif -Decoding meshes\w\w_bonemold_arrow.nif -Decoding meshes\w\w_battleaxe_iron.nif -Decoding meshes\a\a_trollbone_cuir.nif -Decoding meshes\a\a_trollbone_helm.nif -Decoding meshes\w\w_bolt_silver.nif -Decoding meshes\w\w_bolt_orcish.nif -Decoding meshes\w\w_bolt_steel.nif -Decoding meshes\w\w_bolt_iron.nif -Decoding meshes\a\a_tenpaceboot.nif -Decoding meshes\w\w_art_queenofbats.nif -Decoding meshes\w\w_art_dagger_fang.nif -Decoding meshes\w\w_art_spear_mercy.nif -Decoding meshes\w\w_art_staff_magnus.nif -Decoding meshes\w\w_art_mace_scourge.nif -Decoding meshes\w\shadowbluntonehand.nif -Decoding meshes\w\shadowblunttwowide.nif -Decoding meshes\w\shadow_towershield.nif -Decoding meshes\w\shadowspeartwowide.nif -Decoding meshes\w\shadowmarksmanbow.nif -Decoding meshes\w\shadowaxetwoclose.nif -Decoding meshes\a\towershield_steel.nif -Decoding meshes\a\towershield_orcish.nif -Decoding meshes\a\towershield_ebony.nif -Decoding meshes\a\towershield_hlaluu.nif -Decoding meshes\a\towershield_glass.nif -Decoding meshes\a\towershield_chitin.nif -Decoding meshes\c\amulet_expensive_1.nif -Decoding meshes\c\amulet_expensive_3.nif -Decoding meshes\c\amulet_expensive_2.nif -Decoding meshes\c\amulet_exquisit_1.nif -Decoding meshes\a\towershield_iron.nif -Decoding meshes\c\amulet_common_4.nif -Decoding meshes\c\amulet_common_2.nif -Decoding meshes\c\amulet_madstone.nif -Decoding meshes\c\amulet_common_5.nif -Decoding meshes\c\amulet_common_1.nif -Decoding meshes\c\amulet_common_3.nif -Decoding meshes\c\amulet_usheeja.nif -Decoding meshes\w\shadowaxeonehand.nif -Decoding meshes\w\shadow_shield.nif -Decoding meshes\w\shadowshield.nif -Decoding meshes\a\a_watchmanshelm.nif -Decoding meshes\w\w_art_azurastar .nif -Decoding meshes\w\w_art_azurastar.nif -Decoding meshes\w\w_art_volendrung.nif -Decoding meshes\w\w_art_keening.nif -Decoding meshes\w\w_art_sunder.nif -Decoding meshes\x\flora_t_podbud_04.nif -Decoding meshes\x\flora_t_podbud_01.nif -Decoding meshes\x\flora_t_podbud_02.nif -Decoding meshes\x\flora_t_podbud_03.nif -Decoding meshes\r\xnetch_betty.nif -Decoding meshes\r\xnetch_bull.nif -Decoding meshes\x\flora_ashtree_03.nif -Decoding meshes\x\flora_ashtree_02.nif -Decoding meshes\x\flora_ashtree_01.nif -Decoding meshes\x\flora_ashtree_07.nif -Decoding meshes\x\flora_ashtree_06.nif -Decoding meshes\x\flora_ashtree_05.nif -Decoding meshes\x\flora_ashtree_04.nif -Decoding meshes\x\flora_ash_log_03.nif -Decoding meshes\x\flora_ash_log_02.nif -Decoding meshes\x\flora_ash_log_01.nif -Decoding meshes\x\flora_ash_log_04.nif -Decoding meshes\r\xleastkagouti.nif -Decoding meshes\e\fire_shield.nif -Decoding meshes\r\xheart_akulakhan.nif -Decoding meshes\xargonian_swimkna.nif -Decoding meshes\r\steam_centurions.nif -Decoding meshes\r\greatbonewalker.nif diff --git a/components/nif/tests/test.sh b/components/nif/tests/test.sh index 2d07708ad..95ecdbfba 100755 --- a/components/nif/tests/test.sh +++ b/components/nif/tests/test.sh @@ -1,18 +1,15 @@ #!/bin/bash -make || exit +#Script to test all nif files (both loose, and in BSA archives) in data files directory -mkdir -p output +DATAFILESDIR="$1" -PROGS=*_test +find "$DATAFILESDIR" -iname *bsa > nifs.txt +find "$DATAFILESDIR" -iname *nif >> nifs.txt -for a in $PROGS; do - if [ -f "output/$a.out" ]; then - echo "Running $a:" - ./$a | diff output/$a.out - - else - echo "Creating $a.out" - ./$a > "output/$a.out" - git add "output/$a.out" - fi -done +sed -e 's/.*/\"&\"/' nifs.txt > quoted_nifs.txt + +xargs --arg-file=quoted_nifs.txt ../../../niftest + +rm nifs.txt +rm quoted_nifs.txt diff --git a/components/nifbullet/bulletnifloader.cpp b/components/nifbullet/bulletnifloader.cpp index 31d4e10d6..cdc06f985 100644 --- a/components/nifbullet/bulletnifloader.cpp +++ b/components/nifbullet/bulletnifloader.cpp @@ -28,6 +28,8 @@ http://www.gnu.org/licenses/ . #include +#include + #include "../nif/niffile.hpp" #include "../nif/node.hpp" #include "../nif/data.hpp" @@ -46,6 +48,57 @@ http://www.gnu.org/licenses/ . typedef unsigned char ubyte; +// Extract a list of keyframe-controlled nodes from a .kf file +// FIXME: this is a similar copy of OgreNifLoader::loadKf +void extractControlledNodes(Nif::NIFFilePtr kfFile, std::set& controlled) +{ + if(kfFile->numRoots() < 1) + { + kfFile->warn("Found no root nodes in "+kfFile->getFilename()+"."); + return; + } + + const Nif::Record *r = kfFile->getRoot(0); + assert(r != NULL); + + if(r->recType != Nif::RC_NiSequenceStreamHelper) + { + kfFile->warn("First root was not a NiSequenceStreamHelper, but a "+ + r->recName+"."); + return; + } + const Nif::NiSequenceStreamHelper *seq = static_cast(r); + + Nif::ExtraPtr extra = seq->extra; + if(extra.empty() || extra->recType != Nif::RC_NiTextKeyExtraData) + { + kfFile->warn("First extra data was not a NiTextKeyExtraData, but a "+ + (extra.empty() ? std::string("nil") : extra->recName)+"."); + return; + } + + extra = extra->extra; + Nif::ControllerPtr ctrl = seq->controller; + for(;!extra.empty() && !ctrl.empty();(extra=extra->extra),(ctrl=ctrl->next)) + { + if(extra->recType != Nif::RC_NiStringExtraData || ctrl->recType != Nif::RC_NiKeyframeController) + { + kfFile->warn("Unexpected extra data "+extra->recName+" with controller "+ctrl->recName); + continue; + } + + if (!(ctrl->flags & Nif::NiNode::ControllerFlag_Active)) + continue; + + const Nif::NiStringExtraData *strdata = static_cast(extra.getPtr()); + const Nif::NiKeyframeController *key = static_cast(ctrl.getPtr()); + + if(key->data.empty()) + continue; + controlled.insert(strdata->string); + } +} + namespace NifBullet { @@ -70,11 +123,7 @@ void ManualBulletShapeLoader::loadResource(Ogre::Resource *resource) mCompoundShape = NULL; mStaticMesh = NULL; - // Load the NIF. TODO: Wrap this in a try-catch block once we're out - // of the early stages of development. Right now we WANT to catch - // every error as early and intrusively as possible, as it's most - // likely a sign of incomplete code rather than faulty input. - Nif::NIFFile::ptr pnif (Nif::NIFFile::create (mResourceName.substr(0, mResourceName.length()-7))); + Nif::NIFFilePtr pnif (Nif::Cache::getInstance().load(mResourceName.substr(0, mResourceName.length()-7))); Nif::NIFFile & nif = *pnif.get (); if (nif.numRoots() < 1) { @@ -82,6 +131,19 @@ void ManualBulletShapeLoader::loadResource(Ogre::Resource *resource) return; } + // Have to load controlled nodes from the .kf + // FIXME: the .kf has to be loaded both for rendering and physics, ideally it should be opened once and then reused + mControlledNodes.clear(); + std::string kfname = mResourceName.substr(0, mResourceName.length()-7); + Misc::StringUtils::toLower(kfname); + if(kfname.size() > 4 && kfname.compare(kfname.size()-4, 4, ".nif") == 0) + kfname.replace(kfname.size()-4, 4, ".kf"); + if (Ogre::ResourceGroupManager::getSingleton().resourceExistsInAnyGroup(kfname)) + { + Nif::NIFFilePtr kf (Nif::Cache::getInstance().load(kfname)); + extractControlledNodes(kf, mControlledNodes); + } + Nif::Record *r = nif.getRoot(0); assert(r != NULL); @@ -93,10 +155,10 @@ void ManualBulletShapeLoader::loadResource(Ogre::Resource *resource) return; } - mShape->mHasCollisionNode = hasRootCollisionNode(node); + mShape->mAutogenerated = hasAutoGeneratedCollision(node); //do a first pass - handleNode(node,0,false,false,false); + handleNode(node,0,false,false); if(mBoundingBox != NULL) { @@ -131,8 +193,6 @@ void ManualBulletShapeLoader::loadResource(Ogre::Resource *resource) mResourceName = mShape->getName(); mShape->mCollide = false; mBoundingBox = NULL; - mShape->mBoxTranslation = Ogre::Vector3(0,0,0); - mShape->mBoxRotation = Ogre::Quaternion::IDENTITY; mStaticMesh = NULL; mCompoundShape = NULL; @@ -152,12 +212,9 @@ void ManualBulletShapeLoader::loadResource(Ogre::Resource *resource) mShape->mRaycastingShape = new TriangleMeshShape(mStaticMesh,true); } -bool ManualBulletShapeLoader::hasRootCollisionNode(Nif::Node const * node) +bool ManualBulletShapeLoader::hasAutoGeneratedCollision(Nif::Node const * rootNode) { - if(node->recType == Nif::RC_RootCollisionNode) - return true; - - const Nif::NiNode *ninode = dynamic_cast(node); + const Nif::NiNode *ninode = dynamic_cast(rootNode); if(ninode) { const Nif::NodeList &list = ninode->children; @@ -165,18 +222,17 @@ bool ManualBulletShapeLoader::hasRootCollisionNode(Nif::Node const * node) { if(!list[i].empty()) { - if(hasRootCollisionNode(list[i].getPtr())) - return true; + if(list[i].getPtr()->recType == Nif::RC_RootCollisionNode) + return false; } } } - - return false; + return true; } void ManualBulletShapeLoader::handleNode(const Nif::Node *node, int flags, bool isCollisionNode, - bool raycasting, bool isMarker, bool isAnimated) + bool raycasting, bool isAnimated) { // Accumulate the flags from all the child nodes. This works for all // the flags we currently use, at least. @@ -186,6 +242,9 @@ void ManualBulletShapeLoader::handleNode(const Nif::Node *node, int flags, && (node->controller->flags & Nif::NiNode::ControllerFlag_Active)) isAnimated = true; + if (mControlledNodes.find(node->name) != mControlledNodes.end()) + isAnimated = true; + if (!raycasting) isCollisionNode = isCollisionNode || (node->recType == Nif::RC_RootCollisionNode); else @@ -195,13 +254,6 @@ void ManualBulletShapeLoader::handleNode(const Nif::Node *node, int flags, if(node->recType == Nif::RC_AvoidNode) flags |= 0x800; - // Marker objects - /// \todo don't do this in the editor - std::string nodename = node->name; - Misc::StringUtils::toLower(nodename); - if (nodename.find("marker") != std::string::npos) - isMarker = true; - // Check for extra data Nif::Extra const *e = node; while (!e->extra.empty()) @@ -222,16 +274,16 @@ void ManualBulletShapeLoader::handleNode(const Nif::Node *node, int flags, // No collision. Use an internal flag setting to mark this. flags |= 0x800; } - else if (sd->string == "MRK") - // Marker objects. These are only visible in the - // editor. Until and unless we add an editor component to - // the engine, just skip this entire node. - isMarker = true; + else if (sd->string == "MRK" && !mShowMarkers && raycasting) + { + // Marker objects should be invisible, but still have collision. + // Except in the editor, the marker objects are visible. + return; + } } } - if ( (isCollisionNode || (!mShape->mHasCollisionNode && !raycasting)) - && (!isMarker || (mShape->mHasCollisionNode && !raycasting))) + if (isCollisionNode || (mShape->mAutogenerated && !raycasting)) { // NOTE: a trishape with hasBounds=true, but no BBoxCollision flag should NOT go through handleNiTriShape! // It must be ignored completely. @@ -260,7 +312,7 @@ void ManualBulletShapeLoader::handleNode(const Nif::Node *node, int flags, for(size_t i = 0;i < list.length();i++) { if(!list[i].empty()) - handleNode(list[i].getPtr(), flags, isCollisionNode, raycasting, isMarker, isAnimated); + handleNode(list[i].getPtr(), flags, isCollisionNode, raycasting, isAnimated); } } } @@ -279,8 +331,6 @@ void ManualBulletShapeLoader::handleNiTriShape(const Nif::NiTriShape *shape, int // anything. So don't do anything. if ((flags & 0x800) && !raycasting) { - collide = false; - bbcollide = false; return; } @@ -317,16 +367,23 @@ void ManualBulletShapeLoader::handleNiTriShape(const Nif::NiTriShape *shape, int TriangleMeshShape* childShape = new TriangleMeshShape(childMesh,true); - childShape->setLocalScaling(btVector3(transform[0][0], transform[1][1], transform[2][2])); - + float scale = shape->trafo.scale; + const Nif::Node* parent = shape; + while (parent->parent) + { + parent = parent->parent; + scale *= parent->trafo.scale; + } Ogre::Quaternion q = transform.extractQuaternion(); Ogre::Vector3 v = transform.getTrans(); + childShape->setLocalScaling(btVector3(scale, scale, scale)); + btTransform trans(btQuaternion(q.x, q.y, q.z, q.w), btVector3(v.x, v.y, v.z)); if (raycasting) - mShape->mAnimatedRaycastingShapes.insert(std::make_pair(shape->name, mCompoundShape->getNumChildShapes())); + mShape->mAnimatedRaycastingShapes.insert(std::make_pair(shape->recIndex, mCompoundShape->getNumChildShapes())); else - mShape->mAnimatedShapes.insert(std::make_pair(shape->name, mCompoundShape->getNumChildShapes())); + mShape->mAnimatedShapes.insert(std::make_pair(shape->recIndex, mCompoundShape->getNumChildShapes())); mCompoundShape->addChildShape(trans, childShape); } @@ -388,7 +445,7 @@ bool findBoundingBox (const Nif::Node* node, Ogre::Vector3& halfExtents, Ogre::V bool getBoundingBox(const std::string& nifFile, Ogre::Vector3& halfExtents, Ogre::Vector3& translation, Ogre::Quaternion& orientation) { - Nif::NIFFile::ptr pnif (Nif::NIFFile::create (nifFile)); + Nif::NIFFilePtr pnif (Nif::Cache::getInstance().load(nifFile)); Nif::NIFFile & nif = *pnif.get (); if (nif.numRoots() < 1) diff --git a/components/nifbullet/bulletnifloader.hpp b/components/nifbullet/bulletnifloader.hpp index a9ee968b9..56d98834d 100644 --- a/components/nifbullet/bulletnifloader.hpp +++ b/components/nifbullet/bulletnifloader.hpp @@ -67,11 +67,12 @@ struct TriangleMeshShape : public btBvhTriangleMeshShape class ManualBulletShapeLoader : public OEngine::Physic::BulletShapeLoader { public: - ManualBulletShapeLoader() + ManualBulletShapeLoader(bool showMarkers=false) : mShape(NULL) , mStaticMesh(NULL) , mCompoundShape(NULL) , mBoundingBox(NULL) + , mShowMarkers(showMarkers) { } @@ -107,12 +108,12 @@ private: *Parse a node. */ void handleNode(Nif::Node const *node, int flags, bool isCollisionNode, - bool raycasting, bool isMarker, bool isAnimated=false); + bool raycasting, bool isAnimated=false); /** *Helper function */ - bool hasRootCollisionNode(const Nif::Node *node); + bool hasAutoGeneratedCollision(const Nif::Node *rootNode); /** *convert a NiTriShape to a bullet trishape. @@ -128,6 +129,10 @@ private: btTriangleMesh* mStaticMesh; btBoxShape *mBoundingBox; + + std::set mControlledNodes; + + bool mShowMarkers; }; diff --git a/components/nifbullet/test/test.cpp b/components/nifbullet/test/test.cpp deleted file mode 100644 index 261edf512..000000000 --- a/components/nifbullet/test/test.cpp +++ /dev/null @@ -1,209 +0,0 @@ -#include "bullet_nif_loader.hpp" -#include "..\nifogre\ogre_nif_loader.hpp" -#include "..\bsa\bsa_archive.hpp" -#include "..\nifogre\ogre_nif_loader.hpp" -#include -#include -#include -#include -#include "BtOgrePG.h" -#include "BtOgreGP.h" -#include "BtOgreExtras.h" - -const char* mesh = "meshes\\x\\ex_hlaalu_b_24.nif"; - -class MyMotionState : public btMotionState { -public: - MyMotionState(const btTransform &initialpos, Ogre::SceneNode *node) { - mVisibleobj = node; - mPos1 = initialpos; - node->setPosition(initialpos.getOrigin().x(),initialpos.getOrigin().y(),initialpos.getOrigin().z()); - } - - virtual ~MyMotionState() { - } - - void setNode(Ogre::SceneNode *node) { - mVisibleobj = node; - } - - virtual void getWorldTransform(btTransform &worldTrans) const { - worldTrans = mPos1; - } - - virtual void setWorldTransform(const btTransform &worldTrans) { - if(NULL == mVisibleobj) return; // silently return before we set a node - btQuaternion rot = worldTrans.getRotation(); - mVisibleobj->setOrientation(rot.w(), rot.x(), rot.y(), rot.z()); - btVector3 pos = worldTrans.getOrigin(); - mVisibleobj->setPosition(pos.x(), pos.y(), pos.z()); - } - -protected: - Ogre::SceneNode *mVisibleobj; - btTransform mPos1; -}; - - -int main() -{ - try - { - //Ogre stuff - - Ogre::Root* pRoot = new Ogre::Root(); - pRoot->showConfigDialog(); - - BulletShapeManager* manag = new BulletShapeManager(); - - Ogre::RenderWindow* win = pRoot->initialise(true,"test"); - Ogre::SceneManager* scmg = pRoot->createSceneManager(Ogre::ST_GENERIC,"MonGestionnaireDeScene"); - Ogre::Camera* pCamera = scmg->createCamera("test"); - Ogre::Viewport* pViewport = win->addViewport(pCamera); - pCamera->setPosition(-50,0,0); - pCamera->setFarClipDistance(10000); - pCamera->setNearClipDistance(1.); - pCamera->lookAt(0,0,0); - //Ogre::ResourceGroupManager::getSingleton().addResourceLocation("C++/OgreSK/media/models","FileSystem","General"); - Ogre::ResourceGroupManager::getSingleton().addResourceLocation("","FileSystem","General"); - /*Ogre::ResourceGroupManager::getSingleton().addResourceLocation("C++/OgreSK/media/materials/scripts","FileSystem","General"); - Ogre::ResourceGroupManager::getSingleton().addResourceLocation("C++/OgreSK/media/materials/textures","FileSystem","General"); - Ogre::ResourceGroupManager::getSingleton().addResourceLocation("C++/OgreSK/media/materials/programs","FileSystem","General");*/ - - - //OIS stuff - OIS::ParamList pl; - size_t windowHnd = 0; - std::ostringstream windowHndStr; - win->getCustomAttribute("WINDOW", &windowHnd); - windowHndStr << windowHnd; - pl.insert(std::make_pair(std::string("WINDOW"), windowHndStr.str())); - OIS::InputManager *pInputManager = OIS::InputManager::createInputSystem( pl ); - OIS::Mouse *pMouse = static_cast(pInputManager->createInputObject(OIS::OISMouse, false)); - OIS::Keyboard* pKeyboard = static_cast(pInputManager->createInputObject(OIS::OISKeyboard, false)); - unsigned int width, height, depth; - int top, left; - win->getMetrics(width, height, depth, left, top); - const OIS::MouseState &ms = pMouse->getMouseState(); - ms.width = width; - ms.height = height; - - - //Ressources stuff - Bsa::addBSA("Morrowind.bsa"); - //Ogre::ResourceGroupManager::getSingleton().createResourceGroup("general"); - - Ogre::ResourcePtr ptr = BulletShapeManager::getSingleton().getByName(mesh,"General"); - NifBullet::ManualBulletShapeLoader* ShapeLoader = new NifBullet::ManualBulletShapeLoader(); - - ShapeLoader->load(mesh,"General"); - //BulletShapeManager::getSingleton().unload(mesh); - //ShapeLoader->load(mesh,"General"); - - NIFLoader::load(mesh); - - Ogre::ResourceGroupManager::getSingleton().initialiseAllResourceGroups(); - //BulletShapeManager::getSingleton(). - BulletShapePtr shape = BulletShapeManager::getSingleton().getByName(mesh,"General"); - BulletShapeManager::getSingleton().load(mesh,"General"); - BulletShapeManager::getSingleton().unload(mesh); - BulletShapeManager::getSingleton().load(mesh,"General"); - BulletShapeManager::getSingleton().load(mesh,"General"); - //shape->load(); - //shape->unload(); - //shape->load(); - - //Bullet init - btBroadphaseInterface* broadphase = new btDbvtBroadphase(); - - // Set up the collision configuration and dispatcher - btDefaultCollisionConfiguration* collisionConfiguration = new btDefaultCollisionConfiguration(); - btCollisionDispatcher* dispatcher = new btCollisionDispatcher(collisionConfiguration); - - // The actual physics solver - btSequentialImpulseConstraintSolver* solver = new btSequentialImpulseConstraintSolver; - - // The world. - btDiscreteDynamicsWorld* dynamicsWorld = new btDiscreteDynamicsWorld(dispatcher,broadphase,solver,collisionConfiguration); - dynamicsWorld->setGravity(btVector3(0,-10,0)); - - - - //le sol? - Ogre::SceneNode *node = scmg->getRootSceneNode()->createChildSceneNode("node"); - Ogre::Entity *ent = scmg->createEntity("Mesh1",mesh); - node->attachObject(ent); - MyMotionState* mst = new MyMotionState(btTransform::getIdentity(),node); - - btRigidBody::btRigidBodyConstructionInfo groundRigidBodyCI(0,mst,shape->Shape,btVector3(0,0,0)); - btRigidBody* groundRigidBody = new btRigidBody(groundRigidBodyCI); - dynamicsWorld->addRigidBody(groundRigidBody); - - //une balle: - Ogre::SceneNode *node2 = scmg->getRootSceneNode()->createChildSceneNode("node2"); - Ogre::Entity *ent2 = scmg->createEntity("Mesh2","ogrehead.mesh"); - node2->attachObject(ent2); - node2->setPosition(0,500,0); - btTransform iT; - iT.setIdentity(); - iT.setOrigin(btVector3(0,5000,0)); - MyMotionState* mst2 = new MyMotionState(btTransform::getIdentity(),node2); - - btSphereShape* sphereshape = new btSphereShape(10); - btRigidBody::btRigidBodyConstructionInfo sphereCI(10,mst2,sphereshape,btVector3(0,0,0)); - btRigidBody* sphere = new btRigidBody(sphereCI); - dynamicsWorld->addRigidBody(sphere); - - - //btOgre! - BtOgre::DebugDrawer* mDebugDrawer = new BtOgre::DebugDrawer(scmg->getRootSceneNode(), dynamicsWorld); - dynamicsWorld->setDebugDrawer(mDebugDrawer); - - Ogre::Timer timer; - timer.reset(); - bool cont = true; - while(cont) - { - if(timer.getMilliseconds()>30) - { - pMouse->capture(); - pKeyboard->capture(); - - Ogre::Vector3 a(0,0,0); - - if(pKeyboard->isKeyDown(OIS::KC_UP)) - { - a = a + Ogre::Vector3(0,0,-20); - } - if(pKeyboard->isKeyDown(OIS::KC_DOWN)) - { - a = a + Ogre::Vector3(0,0,20); - } - if(pKeyboard->isKeyDown(OIS::KC_ESCAPE)) - { - cont = false; - } - OIS::MouseState MS = pMouse->getMouseState(); - pCamera->yaw(-Ogre::Degree(MS.X.rel)); - pCamera->pitch(-Ogre::Degree(MS.Y.rel)); - pCamera->moveRelative(a); - - pRoot->renderOneFrame(); - mDebugDrawer->step(); - timer.reset(); - dynamicsWorld->stepSimulation(0.03); - } - } - std::cout << "cool"; - delete manag; - delete pRoot; - char a; - std::cin >> a; - } - catch(Ogre::Exception& e) - { - std::cout << e.getFullDescription(); - char a; - std::cin >> a; - } -} diff --git a/components/nifcache/nifcache.cpp b/components/nifcache/nifcache.cpp new file mode 100644 index 000000000..342251dbc --- /dev/null +++ b/components/nifcache/nifcache.cpp @@ -0,0 +1,40 @@ +#include "nifcache.hpp" + +namespace Nif +{ + +Cache* Cache::sThis = 0; + +Cache& Cache::getInstance() +{ + assert (sThis); + return *sThis; +} + +Cache* Cache::getInstancePtr() +{ + return sThis; +} + +Cache::Cache() +{ + assert (!sThis); + sThis = this; +} + +NIFFilePtr Cache::load(const std::string &filename) +{ + // TODO: normalize file path to make sure we're not loading the same file twice + + LoadedMap::iterator it = mLoadedMap.find(filename); + if (it != mLoadedMap.end()) + return it->second; + else + { + NIFFilePtr file(new Nif::NIFFile(filename)); + mLoadedMap[filename] = file; + return file; + } +} + +} diff --git a/components/nifcache/nifcache.hpp b/components/nifcache/nifcache.hpp new file mode 100644 index 000000000..173b91865 --- /dev/null +++ b/components/nifcache/nifcache.hpp @@ -0,0 +1,50 @@ +#ifndef OPENMW_COMPONENTS_NIFCACHE_H +#define OPENMW_COMPONENTS_NIFCACHE_H + +#include + +#include + +#include + +namespace Nif +{ + + typedef boost::shared_ptr NIFFilePtr; + + /// @brief A basic resource manager for NIF files + class Cache + { + public: + Cache(); + + /// Queue this file for background loading. A worker thread will start loading the file. + /// To get the loaded NIFFilePtr, use the load method, which will wait until the worker thread is finished + /// and then return the loaded file. + //void loadInBackground (const std::string& file); + + /// Read and parse the given file. May retrieve from cache if this file has been used previously. + /// @note If the file is currently loading in the background, this function will block until + /// the background loading finishes, then return the background loaded file. + /// @note Returns a SharedPtr to the file and the file will stay loaded as long as the user holds on to this pointer. + /// When all external SharedPtrs to a file are released, the cache may decide to unload the file. + NIFFilePtr load (const std::string& filename); + + /// Return instance of this class. + static Cache& getInstance(); + static Cache* getInstancePtr(); + + private: + static Cache* sThis; + + Cache(const Cache&); + Cache& operator =(const Cache&); + + typedef std::map LoadedMap; + + LoadedMap mLoadedMap; + }; + +} + +#endif diff --git a/components/nifogre/controller.hpp b/components/nifogre/controller.hpp index 317447d95..cc750ea65 100644 --- a/components/nifogre/controller.hpp +++ b/components/nifogre/controller.hpp @@ -2,6 +2,7 @@ #define COMPONENTS_NIFOGRE_CONTROLLER_H #include +#include #include namespace NifOgre @@ -10,53 +11,55 @@ namespace NifOgre class ValueInterpolator { protected: - float interpKey(const Nif::FloatKeyList::VecType &keys, float time, float def=0.f) const + float interpKey(const Nif::FloatKeyMap::MapType &keys, float time, float def=0.f) const { if (keys.size() == 0) return def; - if(time <= keys.front().mTime) - return keys.front().mValue; + if(time <= keys.begin()->first) + return keys.begin()->second.mValue; - const Nif::FloatKey* keyArray = keys.data(); - size_t size = keys.size(); - - for (size_t i = 1; i < size; ++i) + Nif::FloatKeyMap::MapType::const_iterator it = keys.lower_bound(time); + if (it != keys.end()) { - const Nif::FloatKey* aKey = &keyArray[i]; + float aTime = it->first; + const Nif::FloatKey* aKey = &it->second; + + assert (it != keys.begin()); // Shouldn't happen, was checked at beginning of this function - if(aKey->mTime < time) - continue; + Nif::FloatKeyMap::MapType::const_iterator last = --it; + float aLastTime = last->first; + const Nif::FloatKey* aLastKey = &last->second; - const Nif::FloatKey* aLastKey = &keyArray[i-1]; - float a = (time - aLastKey->mTime) / (aKey->mTime - aLastKey->mTime); + float a = (time - aLastTime) / (aTime - aLastTime); return aLastKey->mValue + ((aKey->mValue - aLastKey->mValue) * a); } - - return keys.back().mValue; + else + return keys.rbegin()->second.mValue; } - Ogre::Vector3 interpKey(const Nif::Vector3KeyList::VecType &keys, float time) const + Ogre::Vector3 interpKey(const Nif::Vector3KeyMap::MapType &keys, float time) const { - if(time <= keys.front().mTime) - return keys.front().mValue; - - const Nif::Vector3Key* keyArray = keys.data(); - size_t size = keys.size(); + if(time <= keys.begin()->first) + return keys.begin()->second.mValue; - for (size_t i = 1; i < size; ++i) + Nif::Vector3KeyMap::MapType::const_iterator it = keys.lower_bound(time); + if (it != keys.end()) { - const Nif::Vector3Key* aKey = &keyArray[i]; + float aTime = it->first; + const Nif::Vector3Key* aKey = &it->second; - if(aKey->mTime < time) - continue; + assert (it != keys.begin()); // Shouldn't happen, was checked at beginning of this function - const Nif::Vector3Key* aLastKey = &keyArray[i-1]; - float a = (time - aLastKey->mTime) / (aKey->mTime - aLastKey->mTime); + Nif::Vector3KeyMap::MapType::const_iterator last = --it; + float aLastTime = last->first; + const Nif::Vector3Key* aLastKey = &last->second; + + float a = (time - aLastTime) / (aTime - aLastTime); return aLastKey->mValue + ((aKey->mValue - aLastKey->mValue) * a); } - - return keys.back().mValue; + else + return keys.rbegin()->second.mValue; } }; @@ -86,6 +89,9 @@ namespace NifOgre { if(mDeltaInput) { + if (mStopTime - mStartTime == 0.f) + return 0.f; + mDeltaCount += value*mFrequency; if(mDeltaCount < mStartTime) mDeltaCount = mStopTime - std::fmod(mStartTime - mDeltaCount, diff --git a/components/nifogre/material.cpp b/components/nifogre/material.cpp index 44831c13b..90930bca8 100644 --- a/components/nifogre/material.cpp +++ b/components/nifogre/material.cpp @@ -1,7 +1,7 @@ #include "material.hpp" #include -#include +#include #include #include @@ -54,47 +54,26 @@ static const char *getTestMode(int mode) return "less_equal"; } - -std::string NIFMaterialLoader::findTextureName(const std::string &filename) +static void setTextureProperties(sh::MaterialInstance* material, const std::string& textureSlotName, const Nif::NiTexturingProperty::Texture& tex) { - /* Bethesda at some point converted all their BSA - * textures from tga to dds for increased load speed, but all - * texture file name references were kept as .tga. - */ - static const char path[] = "textures\\"; - static const char path2[] = "textures/"; - - std::string texname = filename; - Misc::StringUtils::toLower(texname); - - // Apparently, leading separators are allowed - while (texname.size() && (texname[0] == '/' || texname[0] == '\\')) - texname.erase(0, 1); - - if(texname.compare(0, sizeof(path)-1, path) != 0 && - texname.compare(0, sizeof(path2)-1, path2) != 0) - texname = path + texname; - - Ogre::String::size_type pos = texname.rfind('.'); - if(pos != Ogre::String::npos && texname.compare(pos, texname.length() - pos, ".dds") != 0) + material->setProperty(textureSlotName + "UVSet", sh::makeProperty(new sh::IntValue(tex.uvSet))); + const std::string clampMode = textureSlotName + "ClampMode"; + switch (tex.clamp) { - // since we know all (GOTY edition or less) textures end - // in .dds, we change the extension - texname.replace(pos, texname.length(), ".dds"); - - // if it turns out that the above wasn't true in all cases (not for vanilla, but maybe mods) - // verify, and revert if false (this call succeeds quickly, but fails slowly) - if(!Ogre::ResourceGroupManager::getSingleton().resourceExistsInAnyGroup(texname)) - { - texname = filename; - Misc::StringUtils::toLower(texname); - if(texname.compare(0, sizeof(path)-1, path) != 0 && - texname.compare(0, sizeof(path2)-1, path2) != 0) - texname = path + texname; - } + case 0: + material->setProperty(clampMode, sh::makeProperty(new sh::StringValue("clamp clamp"))); + break; + case 1: + material->setProperty(clampMode, sh::makeProperty(new sh::StringValue("clamp wrap"))); + break; + case 2: + material->setProperty(clampMode, sh::makeProperty(new sh::StringValue("wrap clamp"))); + break; + case 3: + default: + material->setProperty(clampMode, sh::makeProperty(new sh::StringValue("wrap wrap"))); + break; } - - return texname; } Ogre::String NIFMaterialLoader::getMaterial(const Nif::ShapeData *shapedata, @@ -106,6 +85,7 @@ Ogre::String NIFMaterialLoader::getMaterial(const Nif::ShapeData *shapedata, const Nif::NiZBufferProperty *zprop, const Nif::NiSpecularProperty *specprop, const Nif::NiWireframeProperty *wireprop, + const Nif::NiStencilProperty *stencilprop, bool &needTangents, bool particleMaterial) { Ogre::MaterialManager &matMgr = Ogre::MaterialManager::getSingleton(); @@ -127,6 +107,7 @@ Ogre::String NIFMaterialLoader::getMaterial(const Nif::ShapeData *shapedata, // Default should be 1, but Bloodmoon's models are broken int specFlags = 0; int wireFlags = 0; + int drawMode = 1; Ogre::String texName[7]; bool vertexColour = (shapedata->colors.size() != 0); @@ -146,7 +127,7 @@ Ogre::String NIFMaterialLoader::getMaterial(const Nif::ShapeData *shapedata, const Nif::NiSourceTexture *st = texprop->textures[i].texture.getPtr(); if(st->external) - texName[i] = findTextureName(st->filename); + texName[i] = Misc::ResourceHelpers::correctTexturePath(st->filename); else warn("Found internal texture, ignoring."); } @@ -226,6 +207,20 @@ Ogre::String NIFMaterialLoader::getMaterial(const Nif::ShapeData *shapedata, } } + if(stencilprop) + { + drawMode = stencilprop->data.drawMode; + if (stencilprop->data.enabled) + warn("Unhandled stencil test in "+name); + + Nif::ControllerPtr ctrls = stencilprop->controller; + while(!ctrls.empty()) + { + warn("Unhandled stencil controller "+ctrls->recName+" in "+name); + ctrls = ctrls->next; + } + } + // Material if(matprop) { @@ -270,8 +265,13 @@ Ogre::String NIFMaterialLoader::getMaterial(const Nif::ShapeData *shapedata, for(int i = 0;i < 7;i++) { if(!texName[i].empty()) + { boost::hash_combine(h, texName[i]); + boost::hash_combine(h, texprop->textures[i].clamp); + boost::hash_combine(h, texprop->textures[i].uvSet); + } } + boost::hash_combine(h, drawMode); boost::hash_combine(h, vertexColour); boost::hash_combine(h, alphaFlags); boost::hash_combine(h, alphaTest); @@ -329,6 +329,13 @@ Ogre::String NIFMaterialLoader::getMaterial(const Nif::ShapeData *shapedata, instance->setProperty("polygon_mode", sh::makeProperty(new sh::StringValue("wireframe"))); } + if (drawMode == 1) + instance->setProperty("cullmode", sh::makeProperty(new sh::StringValue("clockwise"))); + else if (drawMode == 2) + instance->setProperty("cullmode", sh::makeProperty(new sh::StringValue("anticlockwise"))); + else if (drawMode == 3) + instance->setProperty("cullmode", sh::makeProperty(new sh::StringValue("none"))); + instance->setProperty("diffuseMap", sh::makeProperty(texName[Nif::NiTexturingProperty::BaseTexture])); instance->setProperty("normalMap", sh::makeProperty(texName[Nif::NiTexturingProperty::BumpTexture])); instance->setProperty("detailMap", sh::makeProperty(texName[Nif::NiTexturingProperty::DetailTexture])); @@ -337,22 +344,22 @@ Ogre::String NIFMaterialLoader::getMaterial(const Nif::ShapeData *shapedata, if (!texName[Nif::NiTexturingProperty::BaseTexture].empty()) { instance->setProperty("use_diffuse_map", sh::makeProperty(new sh::BooleanValue(true))); - instance->setProperty("diffuseMapUVSet", sh::makeProperty(new sh::IntValue(texprop->textures[Nif::NiTexturingProperty::BaseTexture].uvSet))); + setTextureProperties(instance, "diffuseMap", texprop->textures[Nif::NiTexturingProperty::BaseTexture]); } if (!texName[Nif::NiTexturingProperty::GlowTexture].empty()) { instance->setProperty("use_emissive_map", sh::makeProperty(new sh::BooleanValue(true))); - instance->setProperty("emissiveMapUVSet", sh::makeProperty(new sh::IntValue(texprop->textures[Nif::NiTexturingProperty::GlowTexture].uvSet))); + setTextureProperties(instance, "emissiveMap", texprop->textures[Nif::NiTexturingProperty::GlowTexture]); } if (!texName[Nif::NiTexturingProperty::DetailTexture].empty()) { instance->setProperty("use_detail_map", sh::makeProperty(new sh::BooleanValue(true))); - instance->setProperty("detailMapUVSet", sh::makeProperty(new sh::IntValue(texprop->textures[Nif::NiTexturingProperty::DetailTexture].uvSet))); + setTextureProperties(instance, "detailMap", texprop->textures[Nif::NiTexturingProperty::DetailTexture]); } if (!texName[Nif::NiTexturingProperty::DarkTexture].empty()) { instance->setProperty("use_dark_map", sh::makeProperty(new sh::BooleanValue(true))); - instance->setProperty("darkMapUVSet", sh::makeProperty(new sh::IntValue(texprop->textures[Nif::NiTexturingProperty::DarkTexture].uvSet))); + setTextureProperties(instance, "darkMap", texprop->textures[Nif::NiTexturingProperty::DarkTexture]); } bool useParallax = !texName[Nif::NiTexturingProperty::BumpTexture].empty() @@ -375,7 +382,7 @@ Ogre::String NIFMaterialLoader::getMaterial(const Nif::ShapeData *shapedata, instance->setProperty("has_vertex_colour", sh::makeProperty(new sh::BooleanValue(true))); // Override alpha flags based on our override list (transparency-overrides.cfg) - if (!texName[0].empty()) + if ((alphaFlags&1) && !texName[0].empty()) { NifOverrides::TransparencyResult result = NifOverrides::Overrides::getTransparencyOverride(texName[0]); if (result.first) @@ -398,11 +405,17 @@ Ogre::String NIFMaterialLoader::getMaterial(const Nif::ShapeData *shapedata, if((alphaFlags>>9)&1) { +#ifndef ANDROID std::string reject; reject += getTestMode((alphaFlags>>10)&0x7); reject += " "; reject += Ogre::StringConverter::toString(alphaTest); instance->setProperty("alpha_rejection", sh::makeProperty(new sh::StringValue(reject))); +#else + // alpha test not supported in OpenGL ES 2, use manual implementation in shader + instance->setProperty("alphaTestMode", sh::makeProperty(new sh::IntValue((alphaFlags>>10)&0x7))); + instance->setProperty("alphaTestValue", sh::makeProperty(new sh::FloatValue(alphaTest/255.f))); +#endif } else instance->getMaterial()->setShadowCasterMaterial("openmw_shadowcaster_noalpha"); diff --git a/components/nifogre/material.hpp b/components/nifogre/material.hpp index abe1982eb..6be52d1a5 100644 --- a/components/nifogre/material.hpp +++ b/components/nifogre/material.hpp @@ -18,6 +18,7 @@ namespace Nif class NiZBufferProperty; class NiSpecularProperty; class NiWireframeProperty; + class NiStencilProperty; } namespace NifOgre @@ -29,17 +30,9 @@ class NIFMaterialLoader { std::cerr << "NIFMaterialLoader: Warn: " << msg << std::endl; } - static void fail(const std::string &msg) - { - std::cerr << "NIFMaterialLoader: Fail: "<< msg << std::endl; - abort(); - } - static std::map sMaterialMap; public: - static std::string findTextureName(const std::string &filename); - static Ogre::String getMaterial(const Nif::ShapeData *shapedata, const Ogre::String &name, const Ogre::String &group, const Nif::NiTexturingProperty *texprop, @@ -49,6 +42,7 @@ public: const Nif::NiZBufferProperty *zprop, const Nif::NiSpecularProperty *specprop, const Nif::NiWireframeProperty *wireprop, + const Nif::NiStencilProperty *stencilprop, bool &needTangents, bool particleMaterial=false); }; diff --git a/components/nifogre/mesh.cpp b/components/nifogre/mesh.cpp index 8bebe0589..85c3a7b65 100644 --- a/components/nifogre/mesh.cpp +++ b/components/nifogre/mesh.cpp @@ -15,6 +15,7 @@ #include #include +#include #include #include "material.hpp" @@ -319,13 +320,14 @@ void NIFMeshLoader::createSubMesh(Ogre::Mesh *mesh, const Nif::NiTriShape *shape const Nif::NiZBufferProperty *zprop = NULL; const Nif::NiSpecularProperty *specprop = NULL; const Nif::NiWireframeProperty *wireprop = NULL; + const Nif::NiStencilProperty *stencilprop = NULL; bool needTangents = false; - shape->getProperties(texprop, matprop, alphaprop, vertprop, zprop, specprop, wireprop); + shape->getProperties(texprop, matprop, alphaprop, vertprop, zprop, specprop, wireprop, stencilprop); std::string matname = NIFMaterialLoader::getMaterial(data, mesh->getName(), mGroup, texprop, matprop, alphaprop, vertprop, zprop, specprop, - wireprop, needTangents); + wireprop, stencilprop, needTangents); if(matname.length() > 0) sub->setMaterialName(matname); @@ -380,10 +382,10 @@ NIFMeshLoader::NIFMeshLoader(const std::string &name, const std::string &group, void NIFMeshLoader::loadResource(Ogre::Resource *resource) { - Ogre::Mesh *mesh = dynamic_cast(resource); + Ogre::Mesh *mesh = static_cast(resource); OgreAssert(mesh, "Attempting to load a mesh into a non-mesh resource!"); - Nif::NIFFile::ptr nif = Nif::NIFFile::create(mName); + Nif::NIFFilePtr nif = Nif::Cache::getInstance().load(mName); if(mShapeIndex >= nif->numRecords()) { Ogre::SkeletonManager *skelMgr = Ogre::SkeletonManager::getSingletonPtr(); @@ -393,7 +395,7 @@ void NIFMeshLoader::loadResource(Ogre::Resource *resource) } const Nif::Record *record = nif->getRecord(mShapeIndex); - createSubMesh(mesh, dynamic_cast(record)); + createSubMesh(mesh, static_cast(record)); } diff --git a/components/nifogre/ogrenifloader.cpp b/components/nifogre/ogrenifloader.cpp index 81b2e55d2..17df7a3cd 100644 --- a/components/nifogre/ogrenifloader.cpp +++ b/components/nifogre/ogrenifloader.cpp @@ -45,12 +45,32 @@ #include #include +#include #include +#include #include "skeleton.hpp" #include "material.hpp" #include "mesh.hpp" #include "controller.hpp" +#include "particles.hpp" + +namespace +{ + + void getAllNiNodes(const Nif::Node* node, std::vector& out) + { + const Nif::NiNode* ninode = dynamic_cast(node); + if (ninode) + { + out.push_back(ninode); + for (unsigned int i=0; ichildren.length(); ++i) + if (!ninode->children[i].empty()) + getAllNiNodes(ninode->children[i].getPtr(), out); + } + } + +} namespace NifOgre { @@ -115,6 +135,21 @@ ObjectScene::~ObjectScene() mSkelBase = NULL; } +void ObjectScene::setVisibilityFlags (unsigned int flags) +{ + for (std::vector::iterator iter (mEntities.begin()); iter!=mEntities.end(); + ++iter) + (*iter)->setVisibilityFlags (flags); + + for (std::vector::iterator iter (mParticles.begin()); + iter!=mParticles.end(); ++iter) + (*iter)->setVisibilityFlags (flags); + + for (std::vector::iterator iter (mLights.begin()); iter!=mLights.end(); + ++iter) + (*iter)->setVisibilityFlags (flags); +} + void ObjectScene::rotateBillboardNodes(Ogre::Camera *camera) { for (std::vector::iterator it = mBillboardNodes.begin(); it != mBillboardNodes.end(); ++it) @@ -126,6 +161,38 @@ void ObjectScene::rotateBillboardNodes(Ogre::Camera *camera) } } +void ObjectScene::_notifyAttached() +{ + // convert initial particle positions to world space for world-space particle systems + // this can't be done on creation because the particle system is not in its correct world space position yet + for (std::vector::iterator it = mParticles.begin(); it != mParticles.end(); ++it) + { + Ogre::ParticleSystem* psys = *it; + if (psys->getKeepParticlesInLocalSpace()) + continue; + Ogre::ParticleIterator pi = psys->_getIterator(); + while (!pi.end()) + { + Ogre::Particle *p = pi.getNext(); + +#if OGRE_VERSION >= (1 << 16 | 10 << 8 | 0) + Ogre::Vector3& position = p->mPosition; + Ogre::Vector3& direction = p->mDirection; +#else + Ogre::Vector3& position = p->position; + Ogre::Vector3& direction = p->direction; +#endif + + position = + (psys->getParentNode()->_getDerivedOrientation() * + (psys->getParentNode()->_getDerivedScale() * position)) + + psys->getParentNode()->_getDerivedPosition(); + direction = + (psys->getParentNode()->_getDerivedOrientation() * direction); + } + } +} + // Animates a texture class FlipController { @@ -151,7 +218,7 @@ public: const Nif::NiSourceTexture* tex = ctrl->mSources[i].getPtr(); if (!tex->external) std::cerr << "Warning: Found internal texture, ignoring." << std::endl; - mTextures.push_back(NIFMaterialLoader::findTextureName(tex->filename)); + mTextures.push_back(Misc::ResourceHelpers::correctTexturePath(tex->filename)); } } @@ -202,7 +269,7 @@ public: { private: Ogre::MovableObject* mMovable; - Nif::FloatKeyList mData; + Nif::FloatKeyMap mData; MaterialControllerManager* mMaterialControllerMgr; public: @@ -249,7 +316,7 @@ public: { private: Ogre::MovableObject* mMovable; - Nif::Vector3KeyList mData; + Nif::Vector3KeyMap mData; MaterialControllerManager* mMaterialControllerMgr; public: @@ -374,61 +441,84 @@ public: class Value : public NodeTargetValue, public ValueInterpolator { private: - Nif::QuaternionKeyList mRotations; - Nif::Vector3KeyList mTranslations; - Nif::FloatKeyList mScales; + const Nif::QuaternionKeyMap* mRotations; + const Nif::FloatKeyMap* mXRotations; + const Nif::FloatKeyMap* mYRotations; + const Nif::FloatKeyMap* mZRotations; + const Nif::Vector3KeyMap* mTranslations; + const Nif::FloatKeyMap* mScales; + Nif::NIFFilePtr mNif; // Hold a SharedPtr to make sure key lists stay valid using ValueInterpolator::interpKey; - static Ogre::Quaternion interpKey(const Nif::QuaternionKeyList::VecType &keys, float time) + static Ogre::Quaternion interpKey(const Nif::QuaternionKeyMap::MapType &keys, float time) { - if(time <= keys.front().mTime) - return keys.front().mValue; - - const Nif::QuaternionKey* keyArray = keys.data(); - size_t size = keys.size(); + if(time <= keys.begin()->first) + return keys.begin()->second.mValue; - for (size_t i = 1; i < size; ++i) + Nif::QuaternionKeyMap::MapType::const_iterator it = keys.lower_bound(time); + if (it != keys.end()) { - const Nif::QuaternionKey* aKey = &keyArray[i]; + float aTime = it->first; + const Nif::QuaternionKey* aKey = &it->second; - if(aKey->mTime < time) - continue; + assert (it != keys.begin()); // Shouldn't happen, was checked at beginning of this function - const Nif::QuaternionKey* aLastKey = &keyArray[i-1]; - float a = (time - aLastKey->mTime) / (aKey->mTime - aLastKey->mTime); + Nif::QuaternionKeyMap::MapType::const_iterator last = --it; + float aLastTime = last->first; + const Nif::QuaternionKey* aLastKey = &last->second; + + float a = (time - aLastTime) / (aTime - aLastTime); return Ogre::Quaternion::nlerp(a, aLastKey->mValue, aKey->mValue); } + else + return keys.rbegin()->second.mValue; + } - return keys.back().mValue; + Ogre::Quaternion getXYZRotation(float time) const + { + float xrot = interpKey(mXRotations->mKeys, time); + float yrot = interpKey(mYRotations->mKeys, time); + float zrot = interpKey(mZRotations->mKeys, time); + Ogre::Quaternion xr(Ogre::Radian(xrot), Ogre::Vector3::UNIT_X); + Ogre::Quaternion yr(Ogre::Radian(yrot), Ogre::Vector3::UNIT_Y); + Ogre::Quaternion zr(Ogre::Radian(zrot), Ogre::Vector3::UNIT_Z); + return (zr*yr*xr); } public: - Value(Ogre::Node *target, const Nif::NiKeyframeData *data) + /// @note The NiKeyFrameData must be valid as long as this KeyframeController exists. + Value(Ogre::Node *target, const Nif::NIFFilePtr& nif, const Nif::NiKeyframeData *data) : NodeTargetValue(target) - , mRotations(data->mRotations) - , mTranslations(data->mTranslations) - , mScales(data->mScales) + , mRotations(&data->mRotations) + , mXRotations(&data->mXRotations) + , mYRotations(&data->mYRotations) + , mZRotations(&data->mZRotations) + , mTranslations(&data->mTranslations) + , mScales(&data->mScales) + , mNif(nif) { } virtual Ogre::Quaternion getRotation(float time) const { - if(mRotations.mKeys.size() > 0) - return interpKey(mRotations.mKeys, time); + if(mRotations->mKeys.size() > 0) + return interpKey(mRotations->mKeys, time); + else if (!mXRotations->mKeys.empty() || !mYRotations->mKeys.empty() || !mZRotations->mKeys.empty()) + return getXYZRotation(time); return mNode->getOrientation(); } virtual Ogre::Vector3 getTranslation(float time) const { - if(mTranslations.mKeys.size() > 0) - return interpKey(mTranslations.mKeys, time); + if(mTranslations->mKeys.size() > 0) + return interpKey(mTranslations->mKeys, time); return mNode->getPosition(); } virtual Ogre::Vector3 getScale(float time) const { - if(mScales.mKeys.size() > 0) - return Ogre::Vector3(interpKey(mScales.mKeys, time)); + if(mScales->mKeys.size() > 0) + return Ogre::Vector3(interpKey(mScales->mKeys, time)); return mNode->getScale(); } @@ -440,12 +530,14 @@ public: virtual void setValue(Ogre::Real time) { - if(mRotations.mKeys.size() > 0) - mNode->setOrientation(interpKey(mRotations.mKeys, time)); - if(mTranslations.mKeys.size() > 0) - mNode->setPosition(interpKey(mTranslations.mKeys, time)); - if(mScales.mKeys.size() > 0) - mNode->setScale(Ogre::Vector3(interpKey(mScales.mKeys, time))); + if(mRotations->mKeys.size() > 0) + mNode->setOrientation(interpKey(mRotations->mKeys, time)); + else if (!mXRotations->mKeys.empty() || !mYRotations->mKeys.empty() || !mZRotations->mKeys.empty()) + mNode->setOrientation(getXYZRotation(time)); + if(mTranslations->mKeys.size() > 0) + mNode->setPosition(interpKey(mTranslations->mKeys, time)); + if(mScales->mKeys.size() > 0) + mNode->setScale(Ogre::Vector3(interpKey(mScales->mKeys, time))); } }; @@ -459,10 +551,10 @@ public: { private: Ogre::MovableObject* mMovable; - Nif::FloatKeyList mUTrans; - Nif::FloatKeyList mVTrans; - Nif::FloatKeyList mUScale; - Nif::FloatKeyList mVScale; + Nif::FloatKeyMap mUTrans; + Nif::FloatKeyMap mVTrans; + Nif::FloatKeyMap mUScale; + Nif::FloatKeyMap mVScale; MaterialControllerManager* mMaterialControllerMgr; public: @@ -597,6 +689,14 @@ public: */ class NIFObjectLoader { + static bool sShowMarkers; +public: + static void setShowMarkers(bool show) + { + sShowMarkers = show; + } +private: + static void warn(const std::string &msg) { std::cerr << "NIFObjectLoader: Warn: " << msg << std::endl; @@ -693,7 +793,8 @@ class NIFObjectLoader const Nif::NiZBufferProperty *zprop = NULL; const Nif::NiSpecularProperty *specprop = NULL; const Nif::NiWireframeProperty *wireprop = NULL; - node->getProperties(texprop, matprop, alphaprop, vertprop, zprop, specprop, wireprop); + const Nif::NiStencilProperty *stencilprop = NULL; + node->getProperties(texprop, matprop, alphaprop, vertprop, zprop, specprop, wireprop, stencilprop); Ogre::ControllerValueRealPtr srcval((animflags&Nif::NiNode::AnimFlag_AutoPlay) ? Ogre::ControllerManager::getSingleton().getFrameTimeSource() : @@ -706,7 +807,7 @@ class NIFObjectLoader { if (ctrls->recType == Nif::RC_NiAlphaController) { - const Nif::NiAlphaController *alphaCtrl = dynamic_cast(ctrls.getPtr()); + const Nif::NiAlphaController *alphaCtrl = static_cast(ctrls.getPtr()); Ogre::ControllerValueRealPtr dstval(OGRE_NEW AlphaController::Value(movable, alphaCtrl->data.getPtr(), &scene->mMaterialControllerMgr)); AlphaController::Function* function = OGRE_NEW AlphaController::Function(alphaCtrl, (animflags&Nif::NiNode::AnimFlag_AutoPlay)); scene->mMaxControllerLength = std::max(function->mStopTime, scene->mMaxControllerLength); @@ -715,7 +816,7 @@ class NIFObjectLoader } else if (ctrls->recType == Nif::RC_NiMaterialColorController) { - const Nif::NiMaterialColorController *matCtrl = dynamic_cast(ctrls.getPtr()); + const Nif::NiMaterialColorController *matCtrl = static_cast(ctrls.getPtr()); Ogre::ControllerValueRealPtr dstval(OGRE_NEW MaterialColorController::Value(movable, matCtrl->data.getPtr(), &scene->mMaterialControllerMgr)); MaterialColorController::Function* function = OGRE_NEW MaterialColorController::Function(matCtrl, (animflags&Nif::NiNode::AnimFlag_AutoPlay)); scene->mMaxControllerLength = std::max(function->mStopTime, scene->mMaxControllerLength); @@ -733,7 +834,7 @@ class NIFObjectLoader { if (ctrls->recType == Nif::RC_NiFlipController) { - const Nif::NiFlipController *flipCtrl = dynamic_cast(ctrls.getPtr()); + const Nif::NiFlipController *flipCtrl = static_cast(ctrls.getPtr()); Ogre::ControllerValueRealPtr dstval(OGRE_NEW FlipController::Value( @@ -801,18 +902,19 @@ class NIFObjectLoader const Nif::NiColorData *clrdata = cl->data.getPtr(); Ogre::ParticleAffector *affector = partsys->addAffector("ColourInterpolator"); - size_t num_colors = std::min(6, clrdata->mKeyList.mKeys.size()); - for(size_t i = 0;i < num_colors;i++) + size_t num_colors = std::min(6, clrdata->mKeyMap.mKeys.size()); + unsigned int i=0; + for (Nif::Vector4KeyMap::MapType::const_iterator it = clrdata->mKeyMap.mKeys.begin(); it != clrdata->mKeyMap.mKeys.end() && i < num_colors; ++it,++i) { Ogre::ColourValue color; - color.r = clrdata->mKeyList.mKeys[i].mValue[0]; - color.g = clrdata->mKeyList.mKeys[i].mValue[1]; - color.b = clrdata->mKeyList.mKeys[i].mValue[2]; - color.a = clrdata->mKeyList.mKeys[i].mValue[3]; + color.r = it->second.mValue[0]; + color.g = it->second.mValue[1]; + color.b = it->second.mValue[2]; + color.a = it->second.mValue[3]; affector->setParameter("colour"+Ogre::StringConverter::toString(i), Ogre::StringConverter::toString(color)); affector->setParameter("time"+Ogre::StringConverter::toString(i), - Ogre::StringConverter::toString(clrdata->mKeyList.mKeys[i].mTime)); + Ogre::StringConverter::toString(it->first)); } } else if(e->recType == Nif::RC_NiParticleRotation) @@ -834,6 +936,8 @@ class NIFObjectLoader particledata = static_cast(partnode)->data.getPtr(); else if(partnode->recType == Nif::RC_NiRotatingParticles) particledata = static_cast(partnode)->data.getPtr(); + else + throw std::runtime_error("Unexpected particle node type"); std::string fullname = name+"@index="+Ogre::StringConverter::toString(partnode->recIndex); if(partnode->name.length() > 0) @@ -849,13 +953,14 @@ class NIFObjectLoader const Nif::NiZBufferProperty *zprop = NULL; const Nif::NiSpecularProperty *specprop = NULL; const Nif::NiWireframeProperty *wireprop = NULL; + const Nif::NiStencilProperty *stencilprop = NULL; bool needTangents = false; - partnode->getProperties(texprop, matprop, alphaprop, vertprop, zprop, specprop, wireprop); + partnode->getProperties(texprop, matprop, alphaprop, vertprop, zprop, specprop, wireprop, stencilprop); partsys->setMaterialName(NIFMaterialLoader::getMaterial(particledata, fullname, group, texprop, matprop, alphaprop, vertprop, zprop, specprop, - wireprop, needTangents, + wireprop, stencilprop, needTangents, // MW doesn't light particles, but the MaterialProperty // used still has lighting, so that must be ignored. true)); @@ -876,18 +981,42 @@ class NIFObjectLoader { const Nif::NiParticleSystemController *partctrl = static_cast(ctrl.getPtr()); - partsys->setDefaultDimensions(partctrl->size*2, partctrl->size*2); + float size = partctrl->size*2; + // HACK: don't allow zero-sized particles which can rarely cause an AABB assertion in Ogre to fail + size = std::max(size, 0.00001f); + partsys->setDefaultDimensions(size, size); if(!partctrl->emitter.empty()) { int trgtid = NIFSkeletonLoader::lookupOgreBoneHandle(name, partctrl->emitter->recIndex); Ogre::Bone *trgtbone = scene->mSkelBase->getSkeleton()->getBone(trgtid); - // Set the emitter bone as user data on the particle system + // Set the emitter bone(s) as user data on the particle system // so the emitters/affectors can access it easily. - partsys->getUserObjectBindings().setUserAny(Ogre::Any(trgtbone)); + std::vector bones; + if (partctrl->recType == Nif::RC_NiBSPArrayController) + { + std::vector nodes; + getAllNiNodes(partctrl->emitter.getPtr(), nodes); + if (nodes.empty()) + throw std::runtime_error("Emitter for NiBSPArrayController must be a NiNode"); + for (unsigned int i=0; imSkelBase->getSkeleton()->getBone( + NIFSkeletonLoader::lookupOgreBoneHandle(name, nodes[i]->recIndex))); + } + } + else + { + bones.push_back(trgtbone); + } + NiNodeHolder holder; + holder.mBones = bones; + partsys->getUserObjectBindings().setUserAny(Ogre::Any(holder)); createParticleEmitterAffectors(partsys, partctrl, trgtbone, scene->mSkelBase->getName()); } + createParticleInitialState(partsys, particledata, partctrl); + Ogre::ControllerValueRealPtr srcval((partflags&Nif::NiNode::ParticleFlag_AutoPlay) ? Ogre::ControllerManager::getSingleton().getFrameTimeSource() : Ogre::ControllerValueRealPtr()); @@ -900,8 +1029,10 @@ class NIFObjectLoader scene->mControllers.push_back(Ogre::Controller(srcval, dstval, func)); - if (partflags&Nif::NiNode::ParticleFlag_AutoPlay) - partsys->fastForward(1, 0.1); + // Emitting state will be overwritten on frame update by the ParticleSystemController, + // but set up an initial value anyway so the user can fast-forward particle systems + // immediately after creation if desired. + partsys->setEmitting(partflags&Nif::NiNode::ParticleFlag_AutoPlay); } ctrl = ctrl->next; } @@ -912,8 +1043,53 @@ class NIFObjectLoader createMaterialControllers(partnode, partsys, animflags, scene); } + static void createParticleInitialState(Ogre::ParticleSystem* partsys, const Nif::NiAutoNormalParticlesData* particledata, + const Nif::NiParticleSystemController* partctrl) + { + partsys->_update(0.f); // seems to be required to allocate mFreeParticles. TODO: patch Ogre to handle this better + int i=0; + for (std::vector::const_iterator it = partctrl->particles.begin(); + iactiveCount && it != partctrl->particles.end(); ++it, ++i) + { + const Nif::NiParticleSystemController::Particle& particle = *it; + + Ogre::Particle* created = partsys->createParticle(); + if (!created) + break; + +#if OGRE_VERSION >= (1 << 16 | 10 << 8 | 0) + Ogre::Vector3& position = created->mPosition; + Ogre::Vector3& direction = created->mDirection; + Ogre::ColourValue& colour = created->mColour; + float& totalTimeToLive = created->mTotalTimeToLive; + float& timeToLive = created->mTimeToLive; +#else + Ogre::Vector3& position = created->position; + Ogre::Vector3& direction = created->direction; + Ogre::ColourValue& colour = created->colour; + float& totalTimeToLive = created->totalTimeToLive; + float& timeToLive = created->timeToLive; +#endif + + direction = particle.velocity; + position = particledata->vertices.at(particle.vertex); - static void createNodeControllers(const std::string &name, Nif::ControllerPtr ctrl, ObjectScenePtr scene, int animflags) + if (particle.vertex < int(particledata->colors.size())) + { + Ogre::Vector4 partcolour = particledata->colors.at(particle.vertex); + colour = Ogre::ColourValue(partcolour.x, partcolour.y, partcolour.z, partcolour.w); + } + else + colour = Ogre::ColourValue(1.f, 1.f, 1.f, 1.f); + float size = particledata->sizes.at(particle.vertex); + created->setDimensions(size, size); + totalTimeToLive = std::max(0.f, particle.lifespan); + timeToLive = std::max(0.f, particle.lifespan - particle.lifetime); + } + partsys->_update(0.f); // now apparently needs another update, otherwise it won't render in the first frame. TODO: patch Ogre to handle this better + } + + static void createNodeControllers(const Nif::NIFFilePtr& nif, const std::string &name, Nif::ControllerPtr ctrl, ObjectScenePtr scene, int animflags) { do { if (ctrl->flags & Nif::NiNode::ControllerFlag_Active) @@ -947,7 +1123,7 @@ class NIFObjectLoader Ogre::ControllerValueRealPtr srcval((animflags&Nif::NiNode::AnimFlag_AutoPlay) ? Ogre::ControllerManager::getSingleton().getFrameTimeSource() : Ogre::ControllerValueRealPtr()); - Ogre::ControllerValueRealPtr dstval(OGRE_NEW KeyframeController::Value(trgtbone, key->data.getPtr())); + Ogre::ControllerValueRealPtr dstval(OGRE_NEW KeyframeController::Value(trgtbone, nif, key->data.getPtr())); KeyframeController::Function* function = OGRE_NEW KeyframeController::Function(key, (animflags&Nif::NiNode::AnimFlag_AutoPlay)); scene->mMaxControllerLength = std::max(function->mStopTime, scene->mMaxControllerLength); Ogre::ControllerFunctionRealPtr func(function); @@ -987,7 +1163,7 @@ class NIFObjectLoader { std::string::const_iterator last = str.end(); do { - last--; + --last; } while(last != str.begin() && ::isspace(*last)); nextpos = std::distance(str.begin(), ++last); } @@ -1000,18 +1176,13 @@ class NIFObjectLoader } - static void createObjects(const std::string &name, const std::string &group, + static void createObjects(const Nif::NIFFilePtr& nif, const std::string &name, const std::string &group, Ogre::SceneNode *sceneNode, const Nif::Node *node, - ObjectScenePtr scene, int flags, int animflags, int partflags) + ObjectScenePtr scene, int flags, int animflags, int partflags, bool isRootCollisionNode=false) { // Do not create objects for the collision shape (includes all children) if(node->recType == Nif::RC_RootCollisionNode) - return; - - // Marker objects: just skip the entire node branch - /// \todo don't do this in the editor - if (node->name.find("marker") != std::string::npos) - return; + isRootCollisionNode = true; if(node->recType == Nif::RC_NiBSAnimationNode) animflags |= node->flags; @@ -1038,18 +1209,14 @@ class NIFObjectLoader { const Nif::NiTextKeyExtraData *tk = static_cast(e.getPtr()); - if (scene->mSkelBase) - { - int trgtid = NIFSkeletonLoader::lookupOgreBoneHandle(name, node->recIndex); - extractTextKeys(tk, scene->mTextKeys[trgtid]); - } + extractTextKeys(tk, scene->mTextKeys); } else if(e->recType == Nif::RC_NiStringExtraData) { const Nif::NiStringExtraData *sd = static_cast(e.getPtr()); // String markers may contain important information // affecting the entire subtree of this obj - if(sd->string == "MRK") + if(sd->string == "MRK" && !sShowMarkers) { // Marker objects. These meshes are only visible in the // editor. @@ -1061,22 +1228,25 @@ class NIFObjectLoader } if(!node->controller.empty()) - createNodeControllers(name, node->controller, scene, animflags); + createNodeControllers(nif, name, node->controller, scene, animflags); - if(node->recType == Nif::RC_NiCamera) + if (!isRootCollisionNode) { - /* Ignored */ - } + if(node->recType == Nif::RC_NiCamera) + { + /* Ignored */ + } - if(node->recType == Nif::RC_NiTriShape && !(flags&0x80000000)) - { - createEntity(name, group, sceneNode->getCreator(), scene, node, flags, animflags); - } + if(node->recType == Nif::RC_NiTriShape && !(flags&0x80000000)) + { + createEntity(name, group, sceneNode->getCreator(), scene, node, flags, animflags); + } - if((node->recType == Nif::RC_NiAutoNormalParticles || - node->recType == Nif::RC_NiRotatingParticles) && !(flags&0x40000000)) - { - createParticleSystem(name, group, sceneNode, scene, node, flags, partflags, animflags); + if((node->recType == Nif::RC_NiAutoNormalParticles || + node->recType == Nif::RC_NiRotatingParticles) && !(flags&0x40000000)) + { + createParticleSystem(name, group, sceneNode, scene, node, flags, partflags, animflags); + } } const Nif::NiNode *ninode = dynamic_cast(node); @@ -1086,7 +1256,7 @@ class NIFObjectLoader for(size_t i = 0;i < children.length();i++) { if(!children[i].empty()) - createObjects(name, group, sceneNode, children[i].getPtr(), scene, flags, animflags, partflags); + createObjects(nif, name, group, sceneNode, children[i].getPtr(), scene, flags, animflags, partflags, isRootCollisionNode); } } } @@ -1109,7 +1279,7 @@ class NIFObjectLoader public: static void load(Ogre::SceneNode *sceneNode, ObjectScenePtr scene, const std::string &name, const std::string &group, int flags=0) { - Nif::NIFFile::ptr nif = Nif::NIFFile::create(name); + Nif::NIFFilePtr nif = Nif::Cache::getInstance().load(name); if(nif->numRoots() < 1) { nif->warn("Found no root nodes in "+name+"."); @@ -1133,13 +1303,13 @@ public: // Create a base skeleton entity if this NIF needs one createSkelBase(name, group, sceneNode->getCreator(), node, scene); } - createObjects(name, group, sceneNode, node, scene, flags, 0, 0); + createObjects(nif, name, group, sceneNode, node, scene, flags, 0, 0); } static void loadKf(Ogre::Skeleton *skel, const std::string &name, TextKeyMap &textKeys, std::vector > &ctrls) { - Nif::NIFFile::ptr nif = Nif::NIFFile::create(name); + Nif::NIFFilePtr nif = Nif::Cache::getInstance().load(name); if(nif->numRoots() < 1) { nif->warn("Found no root nodes in "+name+"."); @@ -1190,7 +1360,7 @@ public: Ogre::Bone *trgtbone = skel->getBone(strdata->string); Ogre::ControllerValueRealPtr srcval; - Ogre::ControllerValueRealPtr dstval(OGRE_NEW KeyframeController::Value(trgtbone, key->data.getPtr())); + Ogre::ControllerValueRealPtr dstval(OGRE_NEW KeyframeController::Value(trgtbone, nif, key->data.getPtr())); Ogre::ControllerFunctionRealPtr func(OGRE_NEW KeyframeController::Function(key, false)); ctrls.push_back(Ogre::Controller(srcval, dstval, func)); @@ -1201,7 +1371,7 @@ public: ObjectScenePtr Loader::createObjects(Ogre::SceneNode *parentNode, std::string name, const std::string &group) { - ObjectScenePtr scene = ObjectScenePtr (new ObjectScene(parentNode->getCreator()));; + ObjectScenePtr scene = ObjectScenePtr (new ObjectScene(parentNode->getCreator())); Misc::StringUtils::toLower(name); NIFObjectLoader::load(parentNode, scene, name, group); @@ -1213,10 +1383,13 @@ ObjectScenePtr Loader::createObjects(Ogre::SceneNode *parentNode, std::string na parentNode->attachObject(entity); } + scene->_notifyAttached(); + return scene; } ObjectScenePtr Loader::createObjects(Ogre::Entity *parent, const std::string &bonename, + const std::string& bonefilter, Ogre::SceneNode *parentNode, std::string name, const std::string &group) { @@ -1242,11 +1415,9 @@ ObjectScenePtr Loader::createObjects(Ogre::Entity *parent, const std::string &bo if(isskinned) { - // Apparently both are allowed. Sigh. - // This could also mean that filters are supposed to work on the actual node - // hierarchy, rather than just trishapes, and the 'tri ' should be omitted? - std::string filter = "@shape=tri "+bonename; - std::string filter2 = "@shape="+bonename; + // accepts anything named "filter*" or "tri filter*" + std::string filter = "@shape=tri "+bonefilter; + std::string filter2 = "@shape="+bonefilter; Misc::StringUtils::toLower(filter); Misc::StringUtils::toLower(filter2); for(size_t i = 0;i < scene->mEntities.size();i++) @@ -1280,6 +1451,8 @@ ObjectScenePtr Loader::createObjects(Ogre::Entity *parent, const std::string &bo } } + scene->_notifyAttached(); + return scene; } @@ -1306,5 +1479,14 @@ void Loader::createKfControllers(Ogre::Entity *skelBase, NIFObjectLoader::loadKf(skelBase->getSkeleton(), name, textKeys, ctrls); } +bool Loader::sShowMarkers = false; +bool NIFObjectLoader::sShowMarkers = false; + +void Loader::setShowMarkers(bool show) +{ + sShowMarkers = show; + NIFObjectLoader::setShowMarkers(show); +} + } // namespace NifOgre diff --git a/components/nifogre/ogrenifloader.hpp b/components/nifogre/ogrenifloader.hpp index badb6ccd3..c13532644 100644 --- a/components/nifogre/ogrenifloader.hpp +++ b/components/nifogre/ogrenifloader.hpp @@ -69,7 +69,7 @@ struct ObjectScene { // The maximum length on any of the controllers. For animations with controllers, but no text keys, consider this the animation length. float mMaxControllerLength; - std::map mTextKeys; + TextKeyMap mTextKeys; MaterialControllerManager mMaterialControllerMgr; @@ -82,6 +82,12 @@ struct ObjectScene { // Rotate nodes in mBillboardNodes so they face the given camera void rotateBillboardNodes(Ogre::Camera* camera); + + void setVisibilityFlags (unsigned int flags); + + // This is called internally by the OgreNifLoader once all elements of the + // scene have been attached to their respective nodes. + void _notifyAttached(); }; typedef Ogre::SharedPtr ObjectScenePtr; @@ -91,6 +97,7 @@ class Loader { public: static ObjectScenePtr createObjects(Ogre::Entity *parent, const std::string &bonename, + const std::string& filter, Ogre::SceneNode *parentNode, std::string name, const std::string &group="General"); @@ -103,10 +110,18 @@ public: std::string name, const std::string &group="General"); + /// Set whether or not nodes marked as "MRK" should be shown. + /// These should be hidden ingame, but visible in the editior. + /// Default: false. + static void setShowMarkers(bool show); + static void createKfControllers(Ogre::Entity *skelBase, const std::string &name, TextKeyMap &textKeys, std::vector > &ctrls); + +private: + static bool sShowMarkers; }; // FIXME: Should be with other general Ogre extensions. diff --git a/components/nifogre/particles.cpp b/components/nifogre/particles.cpp index 47dce774f..4fec2d29e 100644 --- a/components/nifogre/particles.cpp +++ b/components/nifogre/particles.cpp @@ -16,7 +16,7 @@ class NifEmitter : public Ogre::ParticleEmitter { public: - Ogre::Bone* mEmitterBone; + std::vector mEmitterBones; Ogre::Bone* mParticleBone; Ogre::ParticleSystem* getPartSys() { return mParent; } @@ -130,8 +130,9 @@ public: NifEmitter(Ogre::ParticleSystem *psys) : Ogre::ParticleEmitter(psys) + , mEmitterBones(Ogre::any_cast(psys->getUserObjectBindings().getUserAny()).mBones) { - mEmitterBone = Ogre::any_cast(psys->getUserObjectBindings().getUserAny()); + assert (!mEmitterBones.empty()); Ogre::TagPoint* tag = static_cast(mParent->getParentNode()); mParticleBone = static_cast(tag->getParent()); initDefaults("Nif"); @@ -170,8 +171,10 @@ public: Ogre::Real& timeToLive = particle->timeToLive; #endif + Ogre::Node* emitterBone = mEmitterBones.at((int)(::rand()/(RAND_MAX+1.0)*mEmitterBones.size())); + position = xOff + yOff + zOff + - mParticleBone->_getDerivedOrientation().Inverse() * (mEmitterBone->_getDerivedPosition() + mParticleBone->_getDerivedOrientation().Inverse() * (emitterBone->_getDerivedPosition() - mParticleBone->_getDerivedPosition()); // Generate complex data by reference @@ -181,7 +184,7 @@ public: Ogre::Radian hdir = mHorizontalDir + mHorizontalAngle*Ogre::Math::SymmetricRandom(); Ogre::Radian vdir = mVerticalDir + mVerticalAngle*Ogre::Math::SymmetricRandom(); direction = (mParticleBone->_getDerivedOrientation().Inverse() - * mEmitterBone->_getDerivedOrientation() * + * emitterBone->_getDerivedOrientation() * Ogre::Quaternion(hdir, Ogre::Vector3::UNIT_Z) * Ogre::Quaternion(vdir, Ogre::Vector3::UNIT_X)) * Ogre::Vector3::UNIT_Z; @@ -449,6 +452,8 @@ public: { Ogre::Real scale = (life_time-particle_time) / mGrowTime; assert (scale >= 0); + // HACK: don't allow zero-sized particles which can rarely cause an AABB assertion in Ogre to fail + scale = std::max(scale, 0.00001f); width *= scale; height *= scale; } @@ -456,6 +461,8 @@ public: { Ogre::Real scale = particle_time / mFadeTime; assert (scale >= 0); + // HACK: don't allow zero-sized particles which can rarely cause an AABB assertion in Ogre to fail + scale = std::max(scale, 0.00001f); width *= scale; height *= scale; } @@ -482,6 +489,8 @@ public: { Ogre::Real scale = (life_time-particle_time) / mGrowTime; assert (scale >= 0); + // HACK: don't allow zero-sized particles which can rarely cause an AABB assertion in Ogre to fail + scale = std::max(scale, 0.00001f); width *= scale; height *= scale; } @@ -489,6 +498,8 @@ public: { Ogre::Real scale = particle_time / mFadeTime; assert (scale >= 0); + // HACK: don't allow zero-sized particles which can rarely cause an AABB assertion in Ogre to fail + scale = std::max(scale, 0.00001f); width *= scale; height *= scale; } @@ -635,7 +646,9 @@ public: , mPosition(0.0f) , mDirection(0.0f) { - mEmitterBone = Ogre::any_cast(psys->getUserObjectBindings().getUserAny()); + std::vector bones = Ogre::any_cast(psys->getUserObjectBindings().getUserAny()).mBones; + assert (!bones.empty()); + mEmitterBone = bones[0]; Ogre::TagPoint* tag = static_cast(mParent->getParentNode()); mParticleBone = static_cast(tag->getParent()); diff --git a/components/nifogre/particles.hpp b/components/nifogre/particles.hpp index e1f3fd282..6efc669fe 100644 --- a/components/nifogre/particles.hpp +++ b/components/nifogre/particles.hpp @@ -38,4 +38,13 @@ class GravityAffectorFactory : public Ogre::ParticleAffectorFactory Ogre::ParticleAffector *createAffector(Ogre::ParticleSystem *psys); }; +struct NiNodeHolder +{ + std::vector mBones; + + // Ogre::Any needs this for some reason + friend std::ostream& operator<<(std::ostream& o, const NiNodeHolder& r) + { return o; } +}; + #endif /* OENGINE_OGRE_PARTICLES_H */ diff --git a/components/nifogre/skeleton.cpp b/components/nifogre/skeleton.cpp index c96f03950..db6a753c5 100644 --- a/components/nifogre/skeleton.cpp +++ b/components/nifogre/skeleton.cpp @@ -6,6 +6,7 @@ #include #include +#include #include namespace NifOgre @@ -83,7 +84,7 @@ void NIFSkeletonLoader::loadResource(Ogre::Resource *resource) Ogre::Skeleton *skel = dynamic_cast(resource); OgreAssert(skel, "Attempting to load a skeleton into a non-skeleton resource!"); - Nif::NIFFile::ptr nif(Nif::NIFFile::create(skel->getName())); + Nif::NIFFilePtr nif(Nif::Cache::getInstance().load(skel->getName())); const Nif::Node *node = static_cast(nif->getRoot(0)); try { @@ -102,7 +103,7 @@ bool NIFSkeletonLoader::needSkeleton(const Nif::Node *node) /* We need to be a little aggressive here, since some NIFs have a crap-ton * of nodes and Ogre only supports 256 bones. We will skip a skeleton if: * There are no bones used for skinning, there are no keyframe controllers, there - * are no nodes named "AttachLight", and the tree consists of NiNode, + * are no nodes named "AttachLight" or "ArrowBone", and the tree consists of NiNode, * NiTriShape, and RootCollisionNode types only. */ if(node->boneTrafo) @@ -117,7 +118,7 @@ bool NIFSkeletonLoader::needSkeleton(const Nif::Node *node) } while(!(ctrl=ctrl->next).empty()); } - if (node->name == "AttachLight") + if (node->name == "AttachLight" || node->name == "ArrowBone") return true; if(node->recType == Nif::RC_NiNode || node->recType == Nif::RC_RootCollisionNode) diff --git a/components/ogreinit/ogreinit.cpp b/components/ogreinit/ogreinit.cpp index 515c0875a..574b67f19 100644 --- a/components/ogreinit/ogreinit.cpp +++ b/components/ogreinit/ogreinit.cpp @@ -22,6 +22,7 @@ #include "ogreplugin.hpp" + namespace bfs = boost::filesystem; namespace @@ -44,7 +45,7 @@ namespace LogListener(const std::string &path) : file((bfs::path(path))) { - memset(buffer, sizeof(buffer), 0); + memset(buffer, 0, sizeof(buffer)); } void timestamp() @@ -82,28 +83,36 @@ namespace OgreInit #ifdef ENABLE_PLUGIN_GL , mGLPlugin(NULL) #endif - #ifdef ENABLE_PLUGIN_Direct3D9 + #ifdef ENABLE_PLUGIN_GLES2 + , mGLES2Plugin(NULL) + #endif + + #ifdef ENABLE_PLUGIN_Direct3D9 , mD3D9Plugin(NULL) #endif {} Ogre::Root* OgreInit::init(const std::string &logPath) { + if (mRoot) + throw std::runtime_error("OgreInit was already initialised"); + + #ifndef ANDROID // Set up logging first new Ogre::LogManager; Ogre::Log *log = Ogre::LogManager::getSingleton().createLog(logPath); - #if OGRE_PLATFORM == OGRE_PLATFORM_WIN32 + #if OGRE_PLATFORM == OGRE_PLATFORM_WIN32 // Use custom listener only on Windows log->addListener(new LogListener(logPath)); - #endif + #endif // Disable logging to cout/cerr log->setDebugOutputEnabled(false); - + #endif mRoot = new Ogre::Root("", "", ""); - #if defined(ENABLE_PLUGIN_GL) || defined(ENABLE_PLUGIN_Direct3D9) || defined(ENABLE_PLUGIN_CgProgramManager) || defined(ENABLE_PLUGIN_OctreeSceneManager) || defined(ENABLE_PLUGIN_ParticleFX) + #if defined(ENABLE_PLUGIN_GL) || (ENABLE_PLUGIN_GLES2) || defined(ENABLE_PLUGIN_Direct3D9) || defined(ENABLE_PLUGIN_CgProgramManager) || defined(ENABLE_PLUGIN_OctreeSceneManager) || defined(ENABLE_PLUGIN_ParticleFX) loadStaticPlugins(); #else loadPlugins(); @@ -133,6 +142,10 @@ namespace OgreInit delete mGLPlugin; mGLPlugin = NULL; #endif + #ifdef ENABLE_PLUGIN_GLES2 + delete mGLES2Plugin; + mGLES2Plugin = NULL; + #endif #ifdef ENABLE_PLUGIN_Direct3D9 delete mD3D9Plugin; mD3D9Plugin = NULL; @@ -157,6 +170,10 @@ namespace OgreInit mGLPlugin = new Ogre::GLPlugin(); mRoot->installPlugin(mGLPlugin); #endif + #ifdef ENABLE_PLUGIN_GLES2 + mGLES2Plugin = new Ogre::GLES2Plugin(); + mRoot->installPlugin(mGLES2Plugin); + #endif #ifdef ENABLE_PLUGIN_Direct3D9 mD3D9Plugin = new Ogre::D3D9Plugin(); mRoot->installPlugin(mD3D9Plugin); @@ -201,7 +218,8 @@ namespace OgreInit Files::loadOgrePlugin(pluginDir, "RenderSystem_GL3Plus", *mRoot); Files::loadOgrePlugin(pluginDir, "RenderSystem_Direct3D9", *mRoot); Files::loadOgrePlugin(pluginDir, "Plugin_CgProgramManager", *mRoot); - Files::loadOgrePlugin(pluginDir, "Plugin_ParticleFX", *mRoot); + if (!Files::loadOgrePlugin(pluginDir, "Plugin_ParticleFX", *mRoot)) + throw std::runtime_error("Required Plugin_ParticleFX for Ogre not found!"); } void OgreInit::loadParticleFactories() diff --git a/components/ogreinit/ogreinit.hpp b/components/ogreinit/ogreinit.hpp index b6fe4631a..9613421f7 100644 --- a/components/ogreinit/ogreinit.hpp +++ b/components/ogreinit/ogreinit.hpp @@ -17,6 +17,10 @@ #ifdef ENABLE_PLUGIN_GL # include "OgreGLPlugin.h" #endif +#ifdef ENABLE_PLUGIN_GLES2 +# include "OgreGLES2Plugin.h" +#endif + #ifdef ENABLE_PLUGIN_Direct3D9 # include "OgreD3D9Plugin.h" #endif @@ -52,7 +56,6 @@ namespace OgreInit void loadPlugins(); void loadParticleFactories(); - #ifdef ENABLE_PLUGIN_CgProgramManager Ogre::CgPlugin* mCgPlugin; #endif @@ -65,6 +68,9 @@ namespace OgreInit #ifdef ENABLE_PLUGIN_GL Ogre::GLPlugin* mGLPlugin; #endif + #ifdef ENABLE_PLUGIN_GLES2 + Ogre::GLES2Plugin* mGLES2Plugin; + #endif #ifdef ENABLE_PLUGIN_Direct3D9 Ogre::D3D9Plugin* mD3D9Plugin; #endif diff --git a/components/ogreinit/ogreplugin.cpp b/components/ogreinit/ogreplugin.cpp index 6070c43a8..069b25e7b 100644 --- a/components/ogreinit/ogreplugin.cpp +++ b/components/ogreinit/ogreplugin.cpp @@ -42,4 +42,4 @@ bool loadOgrePlugin(const std::string &pluginDir, std::string pluginName, Ogre:: } } -} \ No newline at end of file +} diff --git a/components/process/processinvoker.cpp b/components/process/processinvoker.cpp new file mode 100644 index 000000000..cc842fd61 --- /dev/null +++ b/components/process/processinvoker.cpp @@ -0,0 +1,185 @@ +#include "processinvoker.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +Process::ProcessInvoker::ProcessInvoker() +{ + mProcess = new QProcess(this); + + connect(mProcess, SIGNAL(error(QProcess::ProcessError)), + this, SLOT(processError(QProcess::ProcessError))); + + connect(mProcess, SIGNAL(finished(int,QProcess::ExitStatus)), + this, SLOT(processFinished(int,QProcess::ExitStatus))); + + + mName = QString(); + mArguments = QStringList(); +} + +Process::ProcessInvoker::~ProcessInvoker() +{ +} + +//void Process::ProcessInvoker::setProcessName(const QString &name) +//{ +// mName = name; +//} + +//void Process::ProcessInvoker::setProcessArguments(const QStringList &arguments) +//{ +// mArguments = arguments; +//} + +QProcess* Process::ProcessInvoker::getProcess() +{ + return mProcess; +} + +//QString Process::ProcessInvoker::getProcessName() +//{ +// return mName; +//} + +//QStringList Process::ProcessInvoker::getProcessArguments() +//{ +// return mArguments; +//} + +bool Process::ProcessInvoker::startProcess(const QString &name, const QStringList &arguments, bool detached) +{ +// mProcess = new QProcess(this); + mName = name; + mArguments = arguments; + + QString path(name); +#ifdef Q_OS_WIN + path.append(QLatin1String(".exe")); +#elif defined(Q_OS_MAC) + QDir dir(QCoreApplication::applicationDirPath()); + path = dir.absoluteFilePath(name); +#else + path.prepend(QLatin1String("./")); +#endif + + QFileInfo info(path); + + if (!info.exists()) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error starting executable")); + msgBox.setIcon(QMessageBox::Warning); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

    Could not find %1

    \ +

    The application is not found.

    \ +

    Please make sure OpenMW is installed correctly and try again.

    ").arg(info.fileName())); + msgBox.exec(); + return false; + } + + if (!info.isExecutable()) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error starting executable")); + msgBox.setIcon(QMessageBox::Warning); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

    Could not start %1

    \ +

    The application is not executable.

    \ +

    Please make sure you have the right permissions and try again.

    ").arg(info.fileName())); + msgBox.exec(); + return false; + } + + // Start the executable + if (detached) { + if (!mProcess->startDetached(path, arguments)) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error starting executable")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

    Could not start %1

    \ +

    An error occurred while starting %1.

    \ +

    Press \"Show Details...\" for more information.

    ").arg(info.fileName())); + msgBox.setDetailedText(mProcess->errorString()); + msgBox.exec(); + return false; + } + } else { + mProcess->start(path, arguments); + + /* + if (!mProcess->waitForFinished()) { + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error starting executable")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

    Could not start %1

    \ +

    An error occurred while starting %1.

    \ +

    Press \"Show Details...\" for more information.

    ").arg(info.fileName())); + msgBox.setDetailedText(mProcess->errorString()); + msgBox.exec(); + + return false; + } + + if (mProcess->exitCode() != 0 || mProcess->exitStatus() == QProcess::CrashExit) { + QString error(mProcess->readAllStandardError()); + error.append(tr("\nArguments:\n")); + error.append(arguments.join(" ")); + + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error running executable")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

    Executable %1 returned an error

    \ +

    An error occurred while running %1.

    \ +

    Press \"Show Details...\" for more information.

    ").arg(info.fileName())); + msgBox.setDetailedText(error); + msgBox.exec(); + + return false; + } + */ + } + + return true; + +} + +void Process::ProcessInvoker::processError(QProcess::ProcessError error) +{ + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error running executable")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

    Executable %1 returned an error

    \ +

    An error occurred while running %1.

    \ +

    Press \"Show Details...\" for more information.

    ").arg(mName)); + msgBox.setDetailedText(mProcess->errorString()); + msgBox.exec(); + +} + +void Process::ProcessInvoker::processFinished(int exitCode, QProcess::ExitStatus exitStatus) +{ + if (exitCode != 0 || exitStatus == QProcess::CrashExit) { + QString error(mProcess->readAllStandardError()); + error.append(tr("\nArguments:\n")); + error.append(mArguments.join(" ")); + + QMessageBox msgBox; + msgBox.setWindowTitle(tr("Error running executable")); + msgBox.setIcon(QMessageBox::Critical); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setText(tr("

    Executable %1 returned an error

    \ +

    An error occurred while running %1.

    \ +

    Press \"Show Details...\" for more information.

    ").arg(mName)); + msgBox.setDetailedText(error); + msgBox.exec(); + } +} diff --git a/components/process/processinvoker.hpp b/components/process/processinvoker.hpp new file mode 100644 index 000000000..8fff6658c --- /dev/null +++ b/components/process/processinvoker.hpp @@ -0,0 +1,43 @@ +#ifndef PROCESSINVOKER_HPP +#define PROCESSINVOKER_HPP + +#include +#include +#include + +namespace Process +{ + class ProcessInvoker : public QObject + { + Q_OBJECT + + public: + + ProcessInvoker(); + ~ProcessInvoker(); + +// void setProcessName(const QString &name); +// void setProcessArguments(const QStringList &arguments); + + QProcess* getProcess(); +// QString getProcessName(); +// QStringList getProcessArguments(); + +// inline bool startProcess(bool detached = false) { return startProcess(mName, mArguments, detached); } + inline bool startProcess(const QString &name, bool detached = false) { return startProcess(name, QStringList(), detached); } + bool startProcess(const QString &name, const QStringList &arguments, bool detached = false); + + private: + QProcess *mProcess; + + QString mName; + QStringList mArguments; + + private slots: + void processError(QProcess::ProcessError error); + void processFinished(int exitCode, QProcess::ExitStatus exitStatus); + + }; +} + +#endif // PROCESSINVOKER_HPP diff --git a/components/settings/settings.cpp b/components/settings/settings.cpp index 0def0afdb..a9a78d035 100644 --- a/components/settings/settings.cpp +++ b/components/settings/settings.cpp @@ -1,157 +1,175 @@ #include "settings.hpp" #include -#include -#include -#include #include -#include -#include - -using namespace Settings; +#include +#include +#include -namespace bfs = boost::filesystem; +namespace Settings +{ -Ogre::ConfigFile Manager::mFile = Ogre::ConfigFile(); -Ogre::ConfigFile Manager::mDefaultFile = Ogre::ConfigFile(); +CategorySettingValueMap Manager::mDefaultSettings = CategorySettingValueMap(); +CategorySettingValueMap Manager::mUserSettings = CategorySettingValueMap(); CategorySettingVector Manager::mChangedSettings = CategorySettingVector(); -CategorySettingValueMap Manager::mNewSettings = CategorySettingValueMap(); - -void Manager::loadUser (const std::string& file) -{ - Ogre::DataStreamPtr stream = openConstrainedFileDataStream(file.c_str()); - mFile.load(stream); -} -void Manager::loadDefault (const std::string& file) -{ - Ogre::DataStreamPtr stream = openConstrainedFileDataStream(file.c_str()); - mDefaultFile.load(stream); -} -void Manager::saveUser(const std::string& file) +class SettingsFileParser { - bfs::ofstream fout((bfs::path(file))); +public: + SettingsFileParser() : mLine(0) {} - Ogre::ConfigFile::SectionIterator seci = mFile.getSectionIterator(); - - while (seci.hasMoreElements()) + void loadSettingsFile (const std::string& file, CategorySettingValueMap& settings) { - Ogre::String sectionName = seci.peekNextKey(); + mFile = file; + boost::filesystem::ifstream stream; + stream.open(boost::filesystem::path(file)); + std::string currentCategory; + mLine = 0; + while (!stream.eof() && !stream.fail()) + { + ++mLine; + std::string line; + std::getline( stream, line ); - if (sectionName.length() > 0) - fout << '\n' << '[' << seci.peekNextKey() << ']' << '\n'; + size_t i = 0; + if (!skipWhiteSpace(i, line)) + continue; - Ogre::ConfigFile::SettingsMultiMap *settings = seci.getNext(); - Ogre::ConfigFile::SettingsMultiMap::iterator i; - for (i = settings->begin(); i != settings->end(); ++i) - { - fout << i->first.c_str() << " = " << i->second.c_str() << '\n'; - } + if (line[i] == '#') // skip comment + continue; - CategorySettingValueMap::iterator it = mNewSettings.begin(); - while (it != mNewSettings.end()) - { - if (it->first.first == sectionName) + if (line[i] == '[') { - fout << it->first.second << " = " << it->second << '\n'; - mNewSettings.erase(it++); + size_t end = line.find(']', i); + if (end == std::string::npos) + fail("unterminated category"); + + currentCategory = line.substr(i+1, end - (i+1)); + boost::algorithm::trim(currentCategory); + i = end+1; } - else - ++it; + + if (!skipWhiteSpace(i, line)) + continue; + + if (currentCategory.empty()) + fail("empty category name"); + + size_t settingEnd = line.find('=', i); + if (settingEnd == std::string::npos) + fail("unterminated setting name"); + + std::string setting = line.substr(i, (settingEnd-i)); + boost::algorithm::trim(setting); + + size_t valueBegin = settingEnd+1; + std::string value = line.substr(valueBegin); + boost::algorithm::trim(value); + + if (settings.insert(std::make_pair(std::make_pair(currentCategory, setting), value)).second == false) + fail(std::string("duplicate setting: [" + currentCategory + "] " + setting)); } } - std::string category = ""; - for (CategorySettingValueMap::iterator it = mNewSettings.begin(); - it != mNewSettings.end(); ++it) +private: + /// Increment i until it longer points to a whitespace character + /// in the string or has reached the end of the string. + /// @return false if we have reached the end of the string + bool skipWhiteSpace(size_t& i, std::string& str) { - if (category != it->first.first) + while (i < str.size() && std::isspace(str[i], std::locale::classic())) { - category = it->first.first; - fout << '\n' << '[' << category << ']' << '\n'; + ++i; } - fout << it->first.second << " = " << it->second << '\n'; + return i < str.size(); } - fout.close(); + void fail(const std::string& message) + { + std::stringstream error; + error << "Error on line " << mLine << " in " << mFile << ":\n" << message; + throw std::runtime_error(error.str()); + } + + std::string mFile; + int mLine; +}; + +void Manager::loadDefault(const std::string &file) +{ + SettingsFileParser parser; + parser.loadSettingsFile(file, mDefaultSettings); } -const std::string Manager::getString (const std::string& setting, const std::string& category) +void Manager::loadUser(const std::string &file) { - if (mNewSettings.find(std::make_pair(category, setting)) != mNewSettings.end()) - return mNewSettings[std::make_pair(category, setting)]; + SettingsFileParser parser; + parser.loadSettingsFile(file, mUserSettings); +} + +void Manager::saveUser(const std::string &file) +{ + boost::filesystem::ofstream stream; + stream.open(boost::filesystem::path(file)); + std::string currentCategory; + for (CategorySettingValueMap::iterator it = mUserSettings.begin(); it != mUserSettings.end(); ++it) + { + if (it->first.first != currentCategory) + { + currentCategory = it->first.first; + stream << "\n[" << currentCategory << "]\n"; + } + stream << it->first.second << " = " << it->second << "\n"; + } +} + +std::string Manager::getString(const std::string &setting, const std::string &category) +{ + CategorySettingValueMap::key_type key = std::make_pair(category, setting); + CategorySettingValueMap::iterator it = mUserSettings.find(key); + if (it != mUserSettings.end()) + return it->second; - std::string defaultval = mDefaultFile.getSetting(setting, category, "NOTFOUND"); - std::string val = mFile.getSetting(setting, category, defaultval); + it = mDefaultSettings.find(key); + if (it != mDefaultSettings.end()) + return it->second; - if (val == "NOTFOUND") - throw std::runtime_error("Trying to retrieve a non-existing setting: " + setting + " Make sure the settings-default.cfg file was properly installed."); - return val; + throw std::runtime_error(std::string("Trying to retrieve a non-existing setting: ") + setting + + ".\nMake sure the settings-default.cfg file file was properly installed."); } -const float Manager::getFloat (const std::string& setting, const std::string& category) +float Manager::getFloat (const std::string& setting, const std::string& category) { return Ogre::StringConverter::parseReal( getString(setting, category) ); } -const int Manager::getInt (const std::string& setting, const std::string& category) +int Manager::getInt (const std::string& setting, const std::string& category) { return Ogre::StringConverter::parseInt( getString(setting, category) ); } -const bool Manager::getBool (const std::string& setting, const std::string& category) +bool Manager::getBool (const std::string& setting, const std::string& category) { return Ogre::StringConverter::parseBool( getString(setting, category) ); } -void Manager::setString (const std::string& setting, const std::string& category, const std::string& value) +void Manager::setString(const std::string &setting, const std::string &category, const std::string &value) { - CategorySetting s = std::make_pair(category, setting); + CategorySettingValueMap::key_type key = std::make_pair(category, setting); - bool found=false; - try + CategorySettingValueMap::iterator found = mUserSettings.find(key); + if (found != mUserSettings.end()) { - Ogre::ConfigFile::SettingsIterator it = mFile.getSettingsIterator(category); - while (it.hasMoreElements()) - { - Ogre::ConfigFile::SettingsMultiMap::iterator i = it.current(); - - if ((*i).first == setting) - { - if ((*i).second != value) - { - mChangedSettings.push_back(std::make_pair(category, setting)); - (*i).second = value; - } - found = true; - } - - it.getNext(); - } + if (found->second == value) + return; } - catch (Ogre::Exception&) - {} - if (!found) - { - if (mNewSettings.find(s) != mNewSettings.end()) - { - if (mNewSettings[s] != value) - { - mChangedSettings.push_back(std::make_pair(category, setting)); - mNewSettings[s] = value; - } - } - else - { - if (mDefaultFile.getSetting(setting, category) != value) - mChangedSettings.push_back(std::make_pair(category, setting)); - mNewSettings[s] = value; - } - } + mUserSettings[key] = value; + + mChangedSettings.insert(key); } void Manager::setInt (const std::string& setting, const std::string& category, const int value) @@ -159,12 +177,12 @@ void Manager::setInt (const std::string& setting, const std::string& category, c setString(setting, category, Ogre::StringConverter::toString(value)); } -void Manager::setFloat (const std::string& setting, const std::string& category, const float value) +void Manager::setFloat (const std::string &setting, const std::string &category, const float value) { setString(setting, category, Ogre::StringConverter::toString(value)); } -void Manager::setBool (const std::string& setting, const std::string& category, const bool value) +void Manager::setBool(const std::string &setting, const std::string &category, const bool value) { setString(setting, category, Ogre::StringConverter::toString(value)); } @@ -175,3 +193,5 @@ const CategorySettingVector Manager::apply() mChangedSettings.clear(); return vec; } + +} diff --git a/components/settings/settings.hpp b/components/settings/settings.hpp index b7c7d59a9..c16ff5a1e 100644 --- a/components/settings/settings.hpp +++ b/components/settings/settings.hpp @@ -1,12 +1,14 @@ #ifndef COMPONENTS_SETTINGS_H #define COMPONENTS_SETTINGS_H -#include +#include +#include +#include namespace Settings { typedef std::pair < std::string, std::string > CategorySetting; - typedef std::vector< std::pair > CategorySettingVector; + typedef std::set< std::pair > CategorySettingVector; typedef std::map < CategorySetting, std::string > CategorySettingValueMap; /// @@ -15,15 +17,12 @@ namespace Settings class Manager { public: - static Ogre::ConfigFile mFile; - static Ogre::ConfigFile mDefaultFile; + static CategorySettingValueMap mDefaultSettings; + static CategorySettingValueMap mUserSettings; static CategorySettingVector mChangedSettings; ///< tracks all the settings that were changed since the last apply() call - static CategorySettingValueMap mNewSettings; - ///< tracks all the settings that are in the default file, but not in user file yet - void loadDefault (const std::string& file); ///< load file as the default settings (can be overridden by user settings) @@ -36,10 +35,10 @@ namespace Settings static const CategorySettingVector apply(); ///< returns the list of changed settings and then clears it - static const int getInt (const std::string& setting, const std::string& category); - static const float getFloat (const std::string& setting, const std::string& category); - static const std::string getString (const std::string& setting, const std::string& category); - static const bool getBool (const std::string& setting, const std::string& category); + static int getInt (const std::string& setting, const std::string& category); + static float getFloat (const std::string& setting, const std::string& category); + static std::string getString (const std::string& setting, const std::string& category); + static bool getBool (const std::string& setting, const std::string& category); static void setInt (const std::string& setting, const std::string& category, const int value); static void setFloat (const std::string& setting, const std::string& category, const float value); diff --git a/components/terrain/buffercache.cpp b/components/terrain/buffercache.cpp index a3e67af5b..01032bcda 100644 --- a/components/terrain/buffercache.cpp +++ b/components/terrain/buffercache.cpp @@ -1,9 +1,186 @@ +/* + * Copyright (c) 2015 scrawl + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ #include "buffercache.hpp" #include #include "defs.hpp" +namespace +{ + +template +Ogre::HardwareIndexBufferSharedPtr createIndexBuffer(unsigned int flags, unsigned int verts, Ogre::HardwareIndexBuffer::IndexType type) +{ + // LOD level n means every 2^n-th vertex is kept + size_t lodLevel = (flags >> (4*4)); + + size_t lodDeltas[4]; + for (int i=0; i<4; ++i) + lodDeltas[i] = (flags >> (4*i)) & (0xf); + + bool anyDeltas = (lodDeltas[Terrain::North] || lodDeltas[Terrain::South] || lodDeltas[Terrain::West] || lodDeltas[Terrain::East]); + + size_t increment = 1 << lodLevel; + assert(increment < verts); + std::vector indices; + indices.reserve((verts-1)*(verts-1)*2*3 / increment); + + size_t rowStart = 0, colStart = 0, rowEnd = verts-1, colEnd = verts-1; + // If any edge needs stitching we'll skip all edges at this point, + // mainly because stitching one edge would have an effect on corners and on the adjacent edges + if (anyDeltas) + { + colStart += increment; + colEnd -= increment; + rowEnd -= increment; + rowStart += increment; + } + for (size_t row = rowStart; row < rowEnd; row += increment) + { + for (size_t col = colStart; col < colEnd; col += increment) + { + indices.push_back(verts*col+row); + indices.push_back(verts*(col+increment)+row+increment); + indices.push_back(verts*col+row+increment); + + indices.push_back(verts*col+row); + indices.push_back(verts*(col+increment)+row); + indices.push_back(verts*(col+increment)+row+increment); + } + } + + size_t innerStep = increment; + if (anyDeltas) + { + // Now configure LOD transitions at the edges - this is pretty tedious, + // and some very long and boring code, but it works great + + // South + size_t row = 0; + size_t outerStep = 1 << (lodDeltas[Terrain::South] + lodLevel); + for (size_t col = 0; col < verts-1; col += outerStep) + { + indices.push_back(verts*col+row); + indices.push_back(verts*(col+outerStep)+row); + // Make sure not to touch the right edge + if (col+outerStep == verts-1) + indices.push_back(verts*(col+outerStep-innerStep)+row+innerStep); + else + indices.push_back(verts*(col+outerStep)+row+innerStep); + + for (size_t i = 0; i < outerStep; i += innerStep) + { + // Make sure not to touch the left or right edges + if (col+i == 0 || col+i == verts-1-innerStep) + continue; + indices.push_back(verts*(col)+row); + indices.push_back(verts*(col+i+innerStep)+row+innerStep); + indices.push_back(verts*(col+i)+row+innerStep); + } + } + + // North + row = verts-1; + outerStep = size_t(1) << (lodDeltas[Terrain::North] + lodLevel); + for (size_t col = 0; col < verts-1; col += outerStep) + { + indices.push_back(verts*(col+outerStep)+row); + indices.push_back(verts*col+row); + // Make sure not to touch the left edge + if (col == 0) + indices.push_back(verts*(col+innerStep)+row-innerStep); + else + indices.push_back(verts*col+row-innerStep); + + for (size_t i = 0; i < outerStep; i += innerStep) + { + // Make sure not to touch the left or right edges + if (col+i == 0 || col+i == verts-1-innerStep) + continue; + indices.push_back(verts*(col+i)+row-innerStep); + indices.push_back(verts*(col+i+innerStep)+row-innerStep); + indices.push_back(verts*(col+outerStep)+row); + } + } + + // West + size_t col = 0; + outerStep = size_t(1) << (lodDeltas[Terrain::West] + lodLevel); + for (size_t row = 0; row < verts-1; row += outerStep) + { + indices.push_back(verts*col+row+outerStep); + indices.push_back(verts*col+row); + // Make sure not to touch the top edge + if (row+outerStep == verts-1) + indices.push_back(verts*(col+innerStep)+row+outerStep-innerStep); + else + indices.push_back(verts*(col+innerStep)+row+outerStep); + + for (size_t i = 0; i < outerStep; i += innerStep) + { + // Make sure not to touch the top or bottom edges + if (row+i == 0 || row+i == verts-1-innerStep) + continue; + indices.push_back(verts*col+row); + indices.push_back(verts*(col+innerStep)+row+i); + indices.push_back(verts*(col+innerStep)+row+i+innerStep); + } + } + + // East + col = verts-1; + outerStep = size_t(1) << (lodDeltas[Terrain::East] + lodLevel); + for (size_t row = 0; row < verts-1; row += outerStep) + { + indices.push_back(verts*col+row); + indices.push_back(verts*col+row+outerStep); + // Make sure not to touch the bottom edge + if (row == 0) + indices.push_back(verts*(col-innerStep)+row+innerStep); + else + indices.push_back(verts*(col-innerStep)+row); + + for (size_t i = 0; i < outerStep; i += innerStep) + { + // Make sure not to touch the top or bottom edges + if (row+i == 0 || row+i == verts-1-innerStep) + continue; + indices.push_back(verts*col+row+outerStep); + indices.push_back(verts*(col-innerStep)+row+i+innerStep); + indices.push_back(verts*(col-innerStep)+row+i); + } + } + } + + Ogre::HardwareBufferManager* mgr = Ogre::HardwareBufferManager::getSingletonPtr(); + Ogre::HardwareIndexBufferSharedPtr buffer = mgr->createIndexBuffer(type, + indices.size(), Ogre::HardwareBuffer::HBU_STATIC); + buffer->writeData(0, buffer->getSizeInBytes(), &indices[0], true); + return buffer; +} + +} + namespace Terrain { @@ -39,7 +216,7 @@ namespace Terrain return buffer; } - Ogre::HardwareIndexBufferSharedPtr BufferCache::getIndexBuffer(int flags) + Ogre::HardwareIndexBufferSharedPtr BufferCache::getIndexBuffer(unsigned int flags) { unsigned int verts = mNumVerts; @@ -48,151 +225,12 @@ namespace Terrain return mIndexBufferMap[flags]; } - // LOD level n means every 2^n-th vertex is kept - size_t lodLevel = (flags >> (4*4)); - - size_t lodDeltas[4]; - for (int i=0; i<4; ++i) - lodDeltas[i] = (flags >> (4*i)) & (0xf); - - bool anyDeltas = (lodDeltas[North] || lodDeltas[South] || lodDeltas[West] || lodDeltas[East]); + Ogre::HardwareIndexBufferSharedPtr buffer; + if (verts*verts > (0xffffu)) + buffer = createIndexBuffer(flags, verts, Ogre::HardwareIndexBuffer::IT_32BIT); + else + buffer = createIndexBuffer(flags, verts, Ogre::HardwareIndexBuffer::IT_16BIT); - size_t increment = 1 << lodLevel; - assert(increment < verts); - std::vector indices; - indices.reserve((verts-1)*(verts-1)*2*3 / increment); - - size_t rowStart = 0, colStart = 0, rowEnd = verts-1, colEnd = verts-1; - // If any edge needs stitching we'll skip all edges at this point, - // mainly because stitching one edge would have an effect on corners and on the adjacent edges - if (anyDeltas) - { - colStart += increment; - colEnd -= increment; - rowEnd -= increment; - rowStart += increment; - } - for (size_t row = rowStart; row < rowEnd; row += increment) - { - for (size_t col = colStart; col < colEnd; col += increment) - { - indices.push_back(verts*col+row); - indices.push_back(verts*(col+increment)+row+increment); - indices.push_back(verts*col+row+increment); - - indices.push_back(verts*col+row); - indices.push_back(verts*(col+increment)+row); - indices.push_back(verts*(col+increment)+row+increment); - } - } - - size_t innerStep = increment; - if (anyDeltas) - { - // Now configure LOD transitions at the edges - this is pretty tedious, - // and some very long and boring code, but it works great - - // South - size_t row = 0; - size_t outerStep = 1 << (lodDeltas[South] + lodLevel); - for (size_t col = 0; col < verts-1; col += outerStep) - { - indices.push_back(verts*col+row); - indices.push_back(verts*(col+outerStep)+row); - // Make sure not to touch the right edge - if (col+outerStep == verts-1) - indices.push_back(verts*(col+outerStep-innerStep)+row+innerStep); - else - indices.push_back(verts*(col+outerStep)+row+innerStep); - - for (size_t i = 0; i < outerStep; i += innerStep) - { - // Make sure not to touch the left or right edges - if (col+i == 0 || col+i == verts-1-innerStep) - continue; - indices.push_back(verts*(col)+row); - indices.push_back(verts*(col+i+innerStep)+row+innerStep); - indices.push_back(verts*(col+i)+row+innerStep); - } - } - - // North - row = verts-1; - outerStep = size_t(1) << (lodDeltas[North] + lodLevel); - for (size_t col = 0; col < verts-1; col += outerStep) - { - indices.push_back(verts*(col+outerStep)+row); - indices.push_back(verts*col+row); - // Make sure not to touch the left edge - if (col == 0) - indices.push_back(verts*(col+innerStep)+row-innerStep); - else - indices.push_back(verts*col+row-innerStep); - - for (size_t i = 0; i < outerStep; i += innerStep) - { - // Make sure not to touch the left or right edges - if (col+i == 0 || col+i == verts-1-innerStep) - continue; - indices.push_back(verts*(col+i)+row-innerStep); - indices.push_back(verts*(col+i+innerStep)+row-innerStep); - indices.push_back(verts*(col+outerStep)+row); - } - } - - // West - size_t col = 0; - outerStep = size_t(1) << (lodDeltas[West] + lodLevel); - for (size_t row = 0; row < verts-1; row += outerStep) - { - indices.push_back(verts*col+row+outerStep); - indices.push_back(verts*col+row); - // Make sure not to touch the top edge - if (row+outerStep == verts-1) - indices.push_back(verts*(col+innerStep)+row+outerStep-innerStep); - else - indices.push_back(verts*(col+innerStep)+row+outerStep); - - for (size_t i = 0; i < outerStep; i += innerStep) - { - // Make sure not to touch the top or bottom edges - if (row+i == 0 || row+i == verts-1-innerStep) - continue; - indices.push_back(verts*col+row); - indices.push_back(verts*(col+innerStep)+row+i); - indices.push_back(verts*(col+innerStep)+row+i+innerStep); - } - } - - // East - col = verts-1; - outerStep = size_t(1) << (lodDeltas[East] + lodLevel); - for (size_t row = 0; row < verts-1; row += outerStep) - { - indices.push_back(verts*col+row); - indices.push_back(verts*col+row+outerStep); - // Make sure not to touch the bottom edge - if (row == 0) - indices.push_back(verts*(col-innerStep)+row+innerStep); - else - indices.push_back(verts*(col-innerStep)+row); - - for (size_t i = 0; i < outerStep; i += innerStep) - { - // Make sure not to touch the top or bottom edges - if (row+i == 0 || row+i == verts-1-innerStep) - continue; - indices.push_back(verts*col+row+outerStep); - indices.push_back(verts*(col-innerStep)+row+i+innerStep); - indices.push_back(verts*(col-innerStep)+row+i); - } - } - } - - Ogre::HardwareBufferManager* mgr = Ogre::HardwareBufferManager::getSingletonPtr(); - Ogre::HardwareIndexBufferSharedPtr buffer = mgr->createIndexBuffer(Ogre::HardwareIndexBuffer::IT_16BIT, - indices.size(), Ogre::HardwareBuffer::HBU_STATIC); - buffer->writeData(0, buffer->getSizeInBytes(), &indices[0], true); mIndexBufferMap[flags] = buffer; return buffer; } diff --git a/components/terrain/buffercache.hpp b/components/terrain/buffercache.hpp index f0aea9bfd..887f0822e 100644 --- a/components/terrain/buffercache.hpp +++ b/components/terrain/buffercache.hpp @@ -1,3 +1,24 @@ +/* + * Copyright (c) 2015 scrawl + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ #ifndef COMPONENTS_TERRAIN_BUFFERCACHE_H #define COMPONENTS_TERRAIN_BUFFERCACHE_H @@ -17,7 +38,7 @@ namespace Terrain /// @param flags first 4*4 bits are LOD deltas on each edge, respectively (4 bits each) /// next 4 bits are LOD level of the index buffer (LOD 0 = don't omit any vertices) - Ogre::HardwareIndexBufferSharedPtr getIndexBuffer (int flags); + Ogre::HardwareIndexBufferSharedPtr getIndexBuffer (unsigned int flags); Ogre::HardwareVertexBufferSharedPtr getUVBuffer (); diff --git a/components/terrain/chunk.cpp b/components/terrain/chunk.cpp index 9c60ee017..cce5abd36 100644 --- a/components/terrain/chunk.cpp +++ b/components/terrain/chunk.cpp @@ -1,3 +1,24 @@ +/* + * Copyright (c) 2015 scrawl + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ #include "chunk.hpp" #include diff --git a/components/terrain/chunk.hpp b/components/terrain/chunk.hpp index 9b2ed76ac..22b4f26ef 100644 --- a/components/terrain/chunk.hpp +++ b/components/terrain/chunk.hpp @@ -1,3 +1,24 @@ +/* + * Copyright (c) 2015 scrawl + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ #ifndef COMPONENTS_TERRAIN_TERRAINBATCH_H #define COMPONENTS_TERRAIN_TERRAINBATCH_H diff --git a/components/terrain/defaultworld.cpp b/components/terrain/defaultworld.cpp index 943658235..e14c64f3a 100644 --- a/components/terrain/defaultworld.cpp +++ b/components/terrain/defaultworld.cpp @@ -1,3 +1,24 @@ +/* + * Copyright (c) 2015 scrawl + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ #include "defaultworld.hpp" #include diff --git a/components/terrain/defaultworld.hpp b/components/terrain/defaultworld.hpp index 8769a0d88..90e8cc2b6 100644 --- a/components/terrain/defaultworld.hpp +++ b/components/terrain/defaultworld.hpp @@ -1,3 +1,24 @@ +/* + * Copyright (c) 2015 scrawl + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ #ifndef COMPONENTS_TERRAIN_H #define COMPONENTS_TERRAIN_H @@ -70,9 +91,9 @@ namespace Terrain private: // Called from a background worker thread - Ogre::WorkQueue::Response* handleRequest(const Ogre::WorkQueue::Request* req, const Ogre::WorkQueue* srcQ); + virtual Ogre::WorkQueue::Response* handleRequest(const Ogre::WorkQueue::Request* req, const Ogre::WorkQueue* srcQ); // Called from the main thread - void handleResponse(const Ogre::WorkQueue::Response* res, const Ogre::WorkQueue* srcQ); + virtual void handleResponse(const Ogre::WorkQueue::Response* res, const Ogre::WorkQueue* srcQ); Ogre::uint16 mWorkQueueChannel; bool mVisible; diff --git a/components/terrain/defs.hpp b/components/terrain/defs.hpp index 685937653..6d173d136 100644 --- a/components/terrain/defs.hpp +++ b/components/terrain/defs.hpp @@ -1,3 +1,24 @@ +/* + * Copyright (c) 2015 scrawl + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ #ifndef COMPONENTS_TERRAIN_DEFS_HPP #define COMPONENTS_TERRAIN_DEFS_HPP diff --git a/components/terrain/material.cpp b/components/terrain/material.cpp index a40e576ed..4ff04ab93 100644 --- a/components/terrain/material.cpp +++ b/components/terrain/material.cpp @@ -1,3 +1,24 @@ +/* + * Copyright (c) 2015 scrawl + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ #include "material.hpp" #include diff --git a/components/terrain/material.hpp b/components/terrain/material.hpp index b9000cb1b..f35080e7c 100644 --- a/components/terrain/material.hpp +++ b/components/terrain/material.hpp @@ -1,3 +1,24 @@ +/* + * Copyright (c) 2015 scrawl + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ #ifndef COMPONENTS_TERRAIN_MATERIAL_H #define COMPONENTS_TERRAIN_MATERIAL_H diff --git a/components/terrain/quadtreenode.cpp b/components/terrain/quadtreenode.cpp index 44974eeb1..5fce5eae9 100644 --- a/components/terrain/quadtreenode.cpp +++ b/components/terrain/quadtreenode.cpp @@ -1,3 +1,24 @@ +/* + * Copyright (c) 2015 scrawl + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ #include "quadtreenode.hpp" #include @@ -73,38 +94,6 @@ namespace return NULL; } - - // Ogre::AxisAlignedBox::distance is broken in 1.8. - Ogre::Real distance(const Ogre::AxisAlignedBox& box, const Ogre::Vector3& v) - { - - if (box.contains(v)) - return 0; - else - { - Ogre::Vector3 maxDist(0,0,0); - const Ogre::Vector3& minimum = box.getMinimum(); - const Ogre::Vector3& maximum = box.getMaximum(); - - if (v.x < minimum.x) - maxDist.x = minimum.x - v.x; - else if (v.x > maximum.x) - maxDist.x = v.x - maximum.x; - - if (v.y < minimum.y) - maxDist.y = minimum.y - v.y; - else if (v.y > maximum.y) - maxDist.y = v.y - maximum.y; - - if (v.z < minimum.z) - maxDist.z = minimum.z - v.z; - else if (v.z > maximum.z) - maxDist.z = v.z - maximum.z; - - return maxDist.length(); - } - } - // Create a 2D quad void makeQuad(Ogre::SceneManager* sceneMgr, float left, float top, float right, float bottom, Ogre::MaterialPtr material) { @@ -270,7 +259,7 @@ bool QuadTreeNode::update(const Ogre::Vector3 &cameraPos) if (mBounds.isNull()) return true; - float dist = distance(mWorldBounds, cameraPos); + float dist = mWorldBounds.distance(cameraPos); // Make sure our scene node is attached if (!mSceneNode->isInSceneGraph()) @@ -447,7 +436,7 @@ void QuadTreeNode::updateIndexBuffers() // Fetch a suitable index buffer (which may be shared) size_t ourLod = getActualLodLevel(); - int flags = 0; + unsigned int flags = 0; for (int i=0; i<4; ++i) { @@ -468,7 +457,7 @@ void QuadTreeNode::updateIndexBuffers() if (lod > 0) { assert (lod - ourLod < (1 << 4)); - flags |= int(lod - ourLod) << (4*i); + flags |= static_cast(lod - ourLod) << (4*i); } } flags |= 0 /*((int)mAdditionalLod)*/ << (4*4); diff --git a/components/terrain/quadtreenode.hpp b/components/terrain/quadtreenode.hpp index 626572701..1ecd6fcd5 100644 --- a/components/terrain/quadtreenode.hpp +++ b/components/terrain/quadtreenode.hpp @@ -1,3 +1,24 @@ +/* + * Copyright (c) 2015 scrawl + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ #ifndef COMPONENTS_TERRAIN_QUADTREENODE_H #define COMPONENTS_TERRAIN_QUADTREENODE_H @@ -98,7 +119,6 @@ namespace Terrain DefaultWorld* getTerrain() { return mTerrain; } /// Adjust LODs for the given camera position, possibly splitting up chunks or merging them. - /// @param force Always choose to render this node, even if not the perfect LOD. /// @return Did we (or all of our children) choose to render? bool update (const Ogre::Vector3& cameraPos); @@ -124,7 +144,6 @@ namespace Terrain /// call this method on their children. /// @note Do not call this before World::areLayersLoaded() == true /// @param area area in image space to put the quad - /// @param quads collect quads here so they can be deleted later void prepareForCompositeMap(Ogre::TRect area); /// Create a chunk for this node from the given data. diff --git a/components/terrain/storage.cpp b/components/terrain/storage.cpp index e69de29bb..14009127d 100644 --- a/components/terrain/storage.cpp +++ b/components/terrain/storage.cpp @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2015 scrawl + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ diff --git a/components/terrain/storage.hpp b/components/terrain/storage.hpp index d3770ea57..7846e91c6 100644 --- a/components/terrain/storage.hpp +++ b/components/terrain/storage.hpp @@ -1,3 +1,24 @@ +/* + * Copyright (c) 2015 scrawl + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ #ifndef COMPONENTS_TERRAIN_STORAGE_H #define COMPONENTS_TERRAIN_STORAGE_H @@ -30,6 +51,8 @@ namespace Terrain /// Fill vertex buffers for a terrain chunk. /// @note May be called from background threads. Make sure to only call thread-safe functions from here! /// @note returned colors need to be in render-system specific format! Use RenderSystem::convertColourValue. + /// @note Vertices should be written in row-major order (a row is defined as parallel to the x-axis). + /// The specified positions should be in local space, i.e. relative to the center of the terrain chunk. /// @param lodLevel LOD level, 0 = most detailed /// @param size size of the terrain chunk in cell units /// @param center center of the chunk in cell units diff --git a/apps/openmw/mwrender/terraingrid.cpp b/components/terrain/terraingrid.cpp similarity index 80% rename from apps/openmw/mwrender/terraingrid.cpp rename to components/terrain/terraingrid.cpp index f2bd92061..269152e61 100644 --- a/apps/openmw/mwrender/terraingrid.cpp +++ b/components/terrain/terraingrid.cpp @@ -1,15 +1,33 @@ +/* + * Copyright (c) 2015 scrawl + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ #include "terraingrid.hpp" #include #include #include -#include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" +#include "chunk.hpp" -#include - -namespace MWRender +namespace Terrain { TerrainGrid::TerrainGrid(Ogre::SceneManager *sceneMgr, Terrain::Storage *storage, int visibilityFlags, bool shaders, Terrain::Alignment align) @@ -145,8 +163,8 @@ void TerrainGrid::setVisible(bool visible) Ogre::AxisAlignedBox TerrainGrid::getWorldBoundingBox (const Ogre::Vector2& center) { - int cellX, cellY; - MWBase::Environment::get().getWorld()->positionToIndex(center.x, center.y, cellX, cellY); + int cellX = std::floor(center.x); + int cellY = std::floor(center.y); Grid::iterator it = mGrid.find(std::make_pair(cellX, cellY)); if (it == mGrid.end()) diff --git a/apps/openmw/mwrender/terraingrid.hpp b/components/terrain/terraingrid.hpp similarity index 67% rename from apps/openmw/mwrender/terraingrid.hpp rename to components/terrain/terraingrid.hpp index 1b5250dcf..97ef6d14d 100644 --- a/apps/openmw/mwrender/terraingrid.hpp +++ b/components/terrain/terraingrid.hpp @@ -1,16 +1,33 @@ -#ifndef OPENMW_MWRENDER_TERRAINGRID_H -#define OPENMW_MWRENDER_TERRAINGRID_H - -#include -#include +/* + * Copyright (c) 2015 scrawl + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#ifndef COMPONENTS_TERRAIN_TERRAINGRID_H +#define COMPONENTS_TERRAIN_TERRAINGRID_H + +#include "world.hpp" +#include "material.hpp" namespace Terrain { class Chunk; -} - -namespace MWRender -{ struct GridElement { diff --git a/components/terrain/world.cpp b/components/terrain/world.cpp index 49fb9b5c9..5cc2647c6 100644 --- a/components/terrain/world.cpp +++ b/components/terrain/world.cpp @@ -1,3 +1,24 @@ +/* + * Copyright (c) 2015 scrawl + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ #include "world.hpp" #include @@ -15,6 +36,8 @@ World::World(Ogre::SceneManager* sceneMgr, , mShaders(shaders) , mAlign(align) , mCache(storage->getCellVertices()) + , mShadows(false) + , mSplitShadows(false) { } diff --git a/components/terrain/world.hpp b/components/terrain/world.hpp index beca7903a..3e63b4c93 100644 --- a/components/terrain/world.hpp +++ b/components/terrain/world.hpp @@ -1,3 +1,24 @@ +/* + * Copyright (c) 2015 scrawl + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ #ifndef COMPONENTS_TERRAIN_WORLD_H #define COMPONENTS_TERRAIN_WORLD_H diff --git a/components/to_utf8/to_utf8.cpp b/components/to_utf8/to_utf8.cpp index c53cf62b5..cb9680441 100644 --- a/components/to_utf8/to_utf8.cpp +++ b/components/to_utf8/to_utf8.cpp @@ -319,7 +319,9 @@ void Utf8Encoder::copyFromArray2(const char*& chp, char* &out) } } + std::ios::fmtflags f(std::cout.flags()); std::cout << "Could not find glyph " << std::hex << (int)ch << " " << (int)ch2 << " " << (int)ch3 << std::endl; + std::cout.flags(f); *(out++) = ch; // Could not find glyph, just put whatever } diff --git a/components/translation/translation.cpp b/components/translation/translation.cpp index 5341240af..51947f6f9 100644 --- a/components/translation/translation.cpp +++ b/components/translation/translation.cpp @@ -6,6 +6,11 @@ namespace Translation { + Storage::Storage() + : mEncoder(NULL) + { + } + void Storage::loadTranslationData(const Files::Collections& dataFileCollections, const std::string& esmFileName) { diff --git a/components/translation/translation.hpp b/components/translation/translation.hpp index bca9ea255..6a3f84ba1 100644 --- a/components/translation/translation.hpp +++ b/components/translation/translation.hpp @@ -9,6 +9,7 @@ namespace Translation class Storage { public: + Storage(); void loadTranslationData(const Files::Collections& dataFileCollections, const std::string& esmFileName); diff --git a/components/widgets/box.cpp b/components/widgets/box.cpp new file mode 100644 index 000000000..0ac2ff7fd --- /dev/null +++ b/components/widgets/box.cpp @@ -0,0 +1,425 @@ +#include "box.hpp" + +namespace Gui +{ + + void AutoSizedWidget::notifySizeChange (MyGUI::Widget* w) + { + MyGUI::Widget * parent = w->getParent(); + if (parent != 0) + { + if (mExpandDirection.isLeft()) + { + int hdiff = getRequestedSize ().width - w->getSize().width; + w->setPosition(w->getPosition() - MyGUI::IntPoint(hdiff, 0)); + } + w->setSize(getRequestedSize ()); + + while (parent != 0) + { + Box * b = dynamic_cast(parent); + if (b) + b->notifyChildrenSizeChanged(); + else + break; + parent = parent->getParent(); + } + } + } + + + MyGUI::IntSize AutoSizedTextBox::getRequestedSize() + { + return getTextSize(); + } + + void AutoSizedTextBox::setCaption(const MyGUI::UString& _value) + { + TextBox::setCaption(_value); + + notifySizeChange (this); + } + + void AutoSizedTextBox::setPropertyOverride(const std::string& _key, const std::string& _value) + { + if (_key == "ExpandDirection") + { + mExpandDirection = MyGUI::Align::parse (_value); + } + else + { + TextBox::setPropertyOverride (_key, _value); + } + } + + MyGUI::IntSize AutoSizedEditBox::getRequestedSize() + { + if (getAlign().isHStretch()) + throw std::runtime_error("AutoSizedEditBox can't have HStretch align (" + getName() + ")"); + return MyGUI::IntSize(getSize().width, getTextSize().height); + } + + void AutoSizedEditBox::setCaption(const MyGUI::UString& _value) + { + EditBox::setCaption(_value); + + notifySizeChange (this); + } + + void AutoSizedEditBox::setPropertyOverride(const std::string& _key, const std::string& _value) + { + if (_key == "ExpandDirection") + { + mExpandDirection = MyGUI::Align::parse (_value); + } + else + { + EditBox::setPropertyOverride (_key, _value); + } + } + + + MyGUI::IntSize AutoSizedButton::getRequestedSize() + { + MyGUI::IntSize padding(24, 8); + if (isUserString("TextPadding")) + padding = MyGUI::IntSize::parse(getUserString("TextPadding")); + + MyGUI::IntSize size = getTextSize() + MyGUI::IntSize(padding.width,padding.height); + return size; + } + + void AutoSizedButton::setCaption(const MyGUI::UString& _value) + { + Button::setCaption(_value); + + notifySizeChange (this); + } + + void AutoSizedButton::setPropertyOverride(const std::string& _key, const std::string& _value) + { + if (_key == "ExpandDirection") + { + mExpandDirection = MyGUI::Align::parse (_value); + } + else + { + Button::setPropertyOverride (_key, _value); + } + } + + Box::Box() + : mSpacing(4) + , mPadding(0) + , mAutoResize(false) + { + + } + + void Box::notifyChildrenSizeChanged () + { + align(); + } + + bool Box::_setPropertyImpl(const std::string& _key, const std::string& _value) + { + if (_key == "Spacing") + mSpacing = MyGUI::utility::parseValue(_value); + else if (_key == "Padding") + mPadding = MyGUI::utility::parseValue(_value); + else if (_key == "AutoResize") + mAutoResize = MyGUI::utility::parseValue(_value); + else + return false; + + return true; + } + + void HBox::align () + { + unsigned int count = getChildCount (); + size_t h_stretched_count = 0; + int total_width = 0; + int total_height = 0; + std::vector< std::pair > sizes; + sizes.resize(count); + + for (unsigned int i = 0; i < count; ++i) + { + MyGUI::Widget* w = getChildAt(i); + bool hstretch = w->getUserString ("HStretch") == "true"; + bool hidden = w->getUserString("Hidden") == "true"; + if (hidden) + continue; + h_stretched_count += hstretch; + AutoSizedWidget* aw = dynamic_cast(w); + if (aw) + { + sizes[i] = std::make_pair(aw->getRequestedSize (), hstretch); + total_width += aw->getRequestedSize ().width; + total_height = std::max(total_height, aw->getRequestedSize ().height); + } + else + { + sizes[i] = std::make_pair(w->getSize(), hstretch); + total_width += w->getSize().width; + if (!(w->getUserString("VStretch") == "true")) + total_height = std::max(total_height, w->getSize().height); + } + + if (i != count-1) + total_width += mSpacing; + } + + if (mAutoResize && (total_width+mPadding*2 != getSize().width || total_height+mPadding*2 != getSize().height)) + { + setSize(MyGUI::IntSize(total_width+mPadding*2, total_height+mPadding*2)); + return; + } + + + int curX = 0; + for (unsigned int i = 0; i < count; ++i) + { + if (i == 0) + curX += mPadding; + + MyGUI::Widget* w = getChildAt(i); + + bool hidden = w->getUserString("Hidden") == "true"; + if (hidden) + continue; + + bool vstretch = w->getUserString ("VStretch") == "true"; + int max_height = getSize().height - mPadding*2; + int height = vstretch ? max_height : sizes[i].first.height; + + MyGUI::IntCoord widgetCoord; + widgetCoord.left = curX; + widgetCoord.top = mPadding + (getSize().height-mPadding*2 - height) / 2; + + int width = 0; + if (sizes[i].second) + { + if (h_stretched_count == 0) + throw std::logic_error("unexpected"); + width = sizes[i].first.width + (getSize().width-mPadding*2 - total_width)/h_stretched_count; + } + else + width = sizes[i].first.width; + + widgetCoord.width = width; + widgetCoord.height = height; + w->setCoord(widgetCoord); + curX += width; + + if (i != count-1) + curX += mSpacing; + } + } + + void HBox::setPropertyOverride(const std::string& _key, const std::string& _value) + { + if (!Box::_setPropertyImpl (_key, _value)) + MyGUI::Widget::setPropertyOverride(_key, _value); + } + + void HBox::setSize (const MyGUI::IntSize& _value) + { + MyGUI::Widget::setSize (_value); + align(); + } + + void HBox::setCoord (const MyGUI::IntCoord& _value) + { + MyGUI::Widget::setCoord (_value); + align(); + } + + void HBox::onWidgetCreated(MyGUI::Widget* _widget) + { + align(); + } + + MyGUI::IntSize HBox::getRequestedSize () + { + MyGUI::IntSize size(0,0); + for (unsigned int i = 0; i < getChildCount (); ++i) + { + bool hidden = getChildAt(i)->getUserString("Hidden") == "true"; + if (hidden) + continue; + + AutoSizedWidget* w = dynamic_cast(getChildAt(i)); + if (w) + { + MyGUI::IntSize requested = w->getRequestedSize (); + size.height = std::max(size.height, requested.height); + size.width = size.width + requested.width; + if (i != getChildCount()-1) + size.width += mSpacing; + } + else + { + MyGUI::IntSize requested = getChildAt(i)->getSize (); + size.height = std::max(size.height, requested.height); + + if (getChildAt(i)->getUserString("HStretch") != "true") + size.width = size.width + requested.width; + + if (i != getChildCount()-1) + size.width += mSpacing; + } + size.height += mPadding*2; + size.width += mPadding*2; + } + return size; + } + + + + + void VBox::align () + { + unsigned int count = getChildCount (); + size_t v_stretched_count = 0; + int total_height = 0; + int total_width = 0; + std::vector< std::pair > sizes; + sizes.resize(count); + for (unsigned int i = 0; i < count; ++i) + { + MyGUI::Widget* w = getChildAt(i); + + bool hidden = w->getUserString("Hidden") == "true"; + if (hidden) + continue; + + bool vstretch = w->getUserString ("VStretch") == "true"; + v_stretched_count += vstretch; + AutoSizedWidget* aw = dynamic_cast(w); + if (aw) + { + sizes[i] = std::make_pair(aw->getRequestedSize (), vstretch); + total_height += aw->getRequestedSize ().height; + total_width = std::max(total_width, aw->getRequestedSize ().width); + } + else + { + sizes[i] = std::make_pair(w->getSize(), vstretch); + total_height += w->getSize().height; + + if (!(w->getUserString("HStretch") == "true")) + total_width = std::max(total_width, w->getSize().width); + } + + if (i != count-1) + total_height += mSpacing; + } + + if (mAutoResize && (total_width+mPadding*2 != getSize().width || total_height+mPadding*2 != getSize().height)) + { + setSize(MyGUI::IntSize(total_width+mPadding*2, total_height+mPadding*2)); + return; + } + + + int curY = 0; + for (unsigned int i = 0; i < count; ++i) + { + if (i==0) + curY += mPadding; + + MyGUI::Widget* w = getChildAt(i); + + bool hidden = w->getUserString("Hidden") == "true"; + if (hidden) + continue; + + bool hstretch = w->getUserString ("HStretch") == "true"; + int maxWidth = getSize().width - mPadding*2; + int width = hstretch ? maxWidth : sizes[i].first.width; + + MyGUI::IntCoord widgetCoord; + widgetCoord.top = curY; + widgetCoord.left = mPadding + (getSize().width-mPadding*2 - width) / 2; + + int height = 0; + if (sizes[i].second) + { + if (v_stretched_count == 0) + throw std::logic_error("unexpected"); + height = sizes[i].first.height + (getSize().height-mPadding*2 - total_height)/v_stretched_count; + } + else + height = sizes[i].first.height; + + widgetCoord.height = height; + widgetCoord.width = width; + w->setCoord(widgetCoord); + curY += height; + + if (i != count-1) + curY += mSpacing; + } + } + + void VBox::setPropertyOverride(const std::string& _key, const std::string& _value) + { + if (!Box::_setPropertyImpl (_key, _value)) + MyGUI::Widget::setPropertyOverride(_key, _value); + } + + void VBox::setSize (const MyGUI::IntSize& _value) + { + MyGUI::Widget::setSize (_value); + align(); + } + + void VBox::setCoord (const MyGUI::IntCoord& _value) + { + MyGUI::Widget::setCoord (_value); + align(); + } + + MyGUI::IntSize VBox::getRequestedSize () + { + MyGUI::IntSize size(0,0); + for (unsigned int i = 0; i < getChildCount (); ++i) + { + bool hidden = getChildAt(i)->getUserString("Hidden") == "true"; + if (hidden) + continue; + + AutoSizedWidget* w = dynamic_cast(getChildAt(i)); + if (w) + { + MyGUI::IntSize requested = w->getRequestedSize (); + size.width = std::max(size.width, requested.width); + size.height = size.height + requested.height; + if (i != getChildCount()-1) + size.height += mSpacing; + } + else + { + MyGUI::IntSize requested = getChildAt(i)->getSize (); + size.width = std::max(size.width, requested.width); + + if (getChildAt(i)->getUserString("VStretch") != "true") + size.height = size.height + requested.height; + + if (i != getChildCount()-1) + size.height += mSpacing; + } + size.height += mPadding*2; + size.width += mPadding*2; + } + return size; + } + + void VBox::onWidgetCreated(MyGUI::Widget* _widget) + { + align(); + } + +} diff --git a/components/widgets/box.hpp b/components/widgets/box.hpp new file mode 100644 index 000000000..ccdc5784b --- /dev/null +++ b/components/widgets/box.hpp @@ -0,0 +1,120 @@ +#ifndef OPENMW_WIDGETS_BOX_H +#define OPENMW_WIDGETS_BOX_H + +#include +#include +#include +#include + +namespace Gui +{ + + class AutoSizedWidget + { + public: + AutoSizedWidget() : mExpandDirection(MyGUI::Align::Right) {} + + virtual MyGUI::IntSize getRequestedSize() = 0; + + protected: + void notifySizeChange(MyGUI::Widget* w); + + MyGUI::Align mExpandDirection; + }; + + class AutoSizedTextBox : public AutoSizedWidget, public MyGUI::TextBox + { + MYGUI_RTTI_DERIVED( AutoSizedTextBox ) + + public: + virtual MyGUI::IntSize getRequestedSize(); + virtual void setCaption(const MyGUI::UString& _value); + + protected: + virtual void setPropertyOverride(const std::string& _key, const std::string& _value); + }; + + class AutoSizedEditBox : public AutoSizedWidget, public MyGUI::EditBox + { + MYGUI_RTTI_DERIVED( AutoSizedEditBox ) + + public: + virtual MyGUI::IntSize getRequestedSize(); + virtual void setCaption(const MyGUI::UString& _value); + + protected: + virtual void setPropertyOverride(const std::string& _key, const std::string& _value); + }; + + class AutoSizedButton : public AutoSizedWidget, public MyGUI::Button + { + MYGUI_RTTI_DERIVED( AutoSizedButton ) + + public: + virtual MyGUI::IntSize getRequestedSize(); + virtual void setCaption(const MyGUI::UString& _value); + + protected: + virtual void setPropertyOverride(const std::string& _key, const std::string& _value); + }; + + /** + * @brief A container widget that automatically sizes its children + * @note the box being an AutoSizedWidget as well allows to put boxes inside a box + */ + class Box : public AutoSizedWidget + { + public: + Box(); + + void notifyChildrenSizeChanged(); + + protected: + virtual void align() = 0; + + virtual bool _setPropertyImpl(const std::string& _key, const std::string& _value); + + int mSpacing; // how much space to put between elements + + int mPadding; // outer padding + + bool mAutoResize; // auto resize the box so that it exactly fits all elements + }; + + class HBox : public Box, public MyGUI::Widget + { + MYGUI_RTTI_DERIVED( HBox ) + + public: + virtual void setSize (const MyGUI::IntSize &_value); + virtual void setCoord (const MyGUI::IntCoord &_value); + + protected: + virtual void align(); + virtual MyGUI::IntSize getRequestedSize(); + + virtual void setPropertyOverride(const std::string& _key, const std::string& _value); + + virtual void onWidgetCreated(MyGUI::Widget* _widget); + }; + + class VBox : public Box, public MyGUI::Widget + { + MYGUI_RTTI_DERIVED( VBox) + + public: + virtual void setSize (const MyGUI::IntSize &_value); + virtual void setCoord (const MyGUI::IntCoord &_value); + + protected: + virtual void align(); + virtual MyGUI::IntSize getRequestedSize(); + + virtual void setPropertyOverride(const std::string& _key, const std::string& _value); + + virtual void onWidgetCreated(MyGUI::Widget* _widget); + }; + +} + +#endif diff --git a/apps/openmw/mwgui/imagebutton.cpp b/components/widgets/imagebutton.cpp similarity index 90% rename from apps/openmw/mwgui/imagebutton.cpp rename to components/widgets/imagebutton.cpp index f2565f5c0..1cd882975 100644 --- a/apps/openmw/mwgui/imagebutton.cpp +++ b/components/widgets/imagebutton.cpp @@ -1,8 +1,8 @@ #include "imagebutton.hpp" -#include +#include -namespace MWGui +namespace Gui { void ImageButton::setPropertyOverride(const std::string &_key, const std::string &_value) @@ -44,8 +44,8 @@ namespace MWGui MyGUI::IntSize ImageButton::getRequestedSize(bool logError) { - Ogre::TexturePtr texture = Ogre::TextureManager::getSingleton().getByName(mImageNormal); - if (texture.isNull()) + MyGUI::ITexture* texture = MyGUI::RenderManager::getInstance().getTexture(mImageNormal); + if (!texture) { if (logError) std::cerr << "ImageButton: can't find " << mImageNormal << std::endl; diff --git a/apps/openmw/mwgui/imagebutton.hpp b/components/widgets/imagebutton.hpp similarity index 98% rename from apps/openmw/mwgui/imagebutton.hpp rename to components/widgets/imagebutton.hpp index f4191a3a5..bed6a2794 100644 --- a/apps/openmw/mwgui/imagebutton.hpp +++ b/components/widgets/imagebutton.hpp @@ -3,7 +3,7 @@ #include -namespace MWGui +namespace Gui { /** diff --git a/components/widgets/list.cpp b/components/widgets/list.cpp new file mode 100644 index 000000000..f3d3a2c28 --- /dev/null +++ b/components/widgets/list.cpp @@ -0,0 +1,160 @@ +#include "list.hpp" + +#include +#include +#include +#include + +namespace Gui +{ + + MWList::MWList() : + mClient(0) + , mScrollView(0) + , mItemHeight(0) + { + } + + void MWList::initialiseOverride() + { + Base::initialiseOverride(); + + assignWidget(mClient, "Client"); + if (mClient == 0) + mClient = this; + + mScrollView = mClient->createWidgetReal( + "MW_ScrollView", MyGUI::FloatCoord(0.0, 0.0, 1.0, 1.0), + MyGUI::Align::Top | MyGUI::Align::Left | MyGUI::Align::Stretch, getName() + "_ScrollView"); + } + + void MWList::addItem(const std::string& name) + { + mItems.push_back(name); + } + + void MWList::addSeparator() + { + mItems.push_back(""); + } + + void MWList::adjustSize() + { + redraw(); + } + + void MWList::redraw(bool scrollbarShown) + { + const int _scrollBarWidth = 20; // fetch this from skin? + const int scrollBarWidth = scrollbarShown ? _scrollBarWidth : 0; + const int spacing = 3; + size_t viewPosition = -mScrollView->getViewOffset().top; + + while (mScrollView->getChildCount()) + { + MyGUI::Gui::getInstance().destroyWidget(mScrollView->getChildAt(0)); + } + + mItemHeight = 0; + int i=0; + for (std::vector::const_iterator it=mItems.begin(); + it!=mItems.end(); ++it) + { + if (*it != "") + { + if (mListItemSkin.empty()) + return; + MyGUI::Button* button = mScrollView->createWidget( + mListItemSkin, MyGUI::IntCoord(0, mItemHeight, mScrollView->getSize().width - scrollBarWidth - 2, 24), + MyGUI::Align::Left | MyGUI::Align::Top, getName() + "_item_" + (*it)); + button->setCaption((*it)); + button->getSubWidgetText()->setWordWrap(true); + button->getSubWidgetText()->setTextAlign(MyGUI::Align::Left); + button->eventMouseWheel += MyGUI::newDelegate(this, &MWList::onMouseWheel); + button->eventMouseButtonClick += MyGUI::newDelegate(this, &MWList::onItemSelected); + + int height = button->getTextSize().height; + button->setSize(MyGUI::IntSize(button->getSize().width, height)); + button->setUserData(i); + + mItemHeight += height + spacing; + } + else + { + MyGUI::ImageBox* separator = mScrollView->createWidget("MW_HLine", + MyGUI::IntCoord(2, mItemHeight, mScrollView->getWidth() - scrollBarWidth - 4, 18), + MyGUI::Align::Left | MyGUI::Align::Top | MyGUI::Align::HStretch); + separator->setNeedMouseFocus(false); + + mItemHeight += 18 + spacing; + } + ++i; + } + + // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden + mScrollView->setVisibleVScroll(false); + mScrollView->setCanvasSize(mClient->getSize().width, std::max(mItemHeight, mClient->getSize().height)); + mScrollView->setVisibleVScroll(true); + + if (!scrollbarShown && mItemHeight > mClient->getSize().height) + redraw(true); + + size_t viewRange = mScrollView->getCanvasSize().height; + if(viewPosition > viewRange) + viewPosition = viewRange; + mScrollView->setViewOffset(MyGUI::IntPoint(0, viewPosition * -1)); + } + + void MWList::setPropertyOverride(const std::string &_key, const std::string &_value) + { + if (_key == "ListItemSkin") + mListItemSkin = _value; + else + Base::setPropertyOverride(_key, _value); + } + + unsigned int MWList::getItemCount() + { + return mItems.size(); + } + + std::string MWList::getItemNameAt(unsigned int at) + { + assert(at < mItems.size() && "List item out of bounds"); + return mItems[at]; + } + + void MWList::removeItem(const std::string& name) + { + assert( std::find(mItems.begin(), mItems.end(), name) != mItems.end() ); + mItems.erase( std::find(mItems.begin(), mItems.end(), name) ); + } + + void MWList::clear() + { + mItems.clear(); + } + + void MWList::onMouseWheel(MyGUI::Widget* _sender, int _rel) + { + //NB view offset is negative + if (mScrollView->getViewOffset().top + _rel*0.3 > 0) + mScrollView->setViewOffset(MyGUI::IntPoint(0, 0)); + else + mScrollView->setViewOffset(MyGUI::IntPoint(0, mScrollView->getViewOffset().top + _rel*0.3)); + } + + void MWList::onItemSelected(MyGUI::Widget* _sender) + { + std::string name = _sender->castType()->getCaption(); + int id = *_sender->getUserData(); + eventItemSelected(name, id); + eventWidgetSelected(_sender); + } + + MyGUI::Button *MWList::getItemWidget(const std::string& name) + { + return mScrollView->findWidget (getName() + "_item_" + name)->castType(); + } + +} diff --git a/components/widgets/list.hpp b/components/widgets/list.hpp new file mode 100644 index 000000000..72c8a733c --- /dev/null +++ b/components/widgets/list.hpp @@ -0,0 +1,70 @@ +#ifndef MWGUI_LIST_HPP +#define MWGUI_LIST_HPP + +#include + +namespace Gui +{ + /** + * \brief a very simple list widget that supports word-wrapping entries + * \note if the width or height of the list changes, you must call adjustSize() method + */ + class MWList : public MyGUI::Widget + { + MYGUI_RTTI_DERIVED(MWList) + public: + MWList(); + + typedef MyGUI::delegates::CMultiDelegate2 EventHandle_StringInt; + typedef MyGUI::delegates::CMultiDelegate1 EventHandle_Widget; + + /** + * Event: Item selected with the mouse. + * signature: void method(std::string itemName, int index) + */ + EventHandle_StringInt eventItemSelected; + + /** + * Event: Item selected with the mouse. + * signature: void method(MyGUI::Widget* sender) + */ + EventHandle_Widget eventWidgetSelected; + + + /** + * Call after the size of the list changed, or items were inserted/removed + */ + void adjustSize(); + + void addItem(const std::string& name); + void addSeparator(); ///< add a seperator between the current and the next item. + void removeItem(const std::string& name); + unsigned int getItemCount(); + std::string getItemNameAt(unsigned int at); ///< \attention if there are separators, this method will return "" at the place where the separator is + void clear(); + + MyGUI::Button* getItemWidget(const std::string& name); + ///< get widget for an item name, useful to set up tooltip + + virtual void setPropertyOverride(const std::string& _key, const std::string& _value); + + protected: + void initialiseOverride(); + + void redraw(bool scrollbarShown = false); + + void onMouseWheel(MyGUI::Widget* _sender, int _rel); + void onItemSelected(MyGUI::Widget* _sender); + + private: + MyGUI::ScrollView* mScrollView; + MyGUI::Widget* mClient; + std::string mListItemSkin; + + std::vector mItems; + + int mItemHeight; // height of all items + }; +} + +#endif diff --git a/components/widgets/numericeditbox.cpp b/components/widgets/numericeditbox.cpp new file mode 100644 index 000000000..5361b3127 --- /dev/null +++ b/components/widgets/numericeditbox.cpp @@ -0,0 +1,74 @@ +#include "numericeditbox.hpp" + +#include + +namespace Gui +{ + + void NumericEditBox::initialiseOverride() + { + Base::initialiseOverride(); + eventEditTextChange += MyGUI::newDelegate(this, &NumericEditBox::onEditTextChange); + + mValue = 0; + setCaption("0"); + } + + void NumericEditBox::shutdownOverride() + { + Base::shutdownOverride(); + eventEditTextChange -= MyGUI::newDelegate(this, &NumericEditBox::onEditTextChange); + } + + void NumericEditBox::onEditTextChange(MyGUI::EditBox *sender) + { + std::string newCaption = sender->getCaption(); + if (newCaption.empty()) + { + return; + } + + try + { + mValue = boost::lexical_cast(newCaption); + int capped = std::min(mMaxValue, std::max(mValue, mMinValue)); + if (capped != mValue) + { + mValue = capped; + setCaption(MyGUI::utility::toString(mValue)); + } + } + catch (boost::bad_lexical_cast&) + { + setCaption(MyGUI::utility::toString(mValue)); + } + + eventValueChanged(mValue); + } + + void NumericEditBox::setValue(int value) + { + if (value != mValue) + { + setCaption(MyGUI::utility::toString(value)); + mValue = value; + } + } + + void NumericEditBox::setMinValue(int minValue) + { + mMinValue = minValue; + } + + void NumericEditBox::setMaxValue(int maxValue) + { + mMaxValue = maxValue; + } + + void NumericEditBox::onKeyLostFocus(MyGUI::Widget* _new) + { + Base::onKeyLostFocus(_new); + setCaption(MyGUI::utility::toString(mValue)); + } + +} diff --git a/components/widgets/numericeditbox.hpp b/components/widgets/numericeditbox.hpp new file mode 100644 index 000000000..20000b3d3 --- /dev/null +++ b/components/widgets/numericeditbox.hpp @@ -0,0 +1,45 @@ +#ifndef OPENMW_NUMERIC_EDIT_BOX_H +#define OPENMW_NUMERIC_EDIT_BOX_H + +#include + +namespace Gui +{ + + /** + * @brief A variant of the EditBox that only allows integer inputs + */ + class NumericEditBox : public MyGUI::EditBox + { + MYGUI_RTTI_DERIVED(NumericEditBox) + + public: + NumericEditBox() + : mValue(0), mMinValue(std::numeric_limits::min()), + mMaxValue(std::numeric_limits::max()) + {} + + void initialiseOverride(); + void shutdownOverride(); + + typedef MyGUI::delegates::CMultiDelegate1 EventHandle_ValueChanged; + EventHandle_ValueChanged eventValueChanged; + + /// @note Does not trigger eventValueChanged + void setValue (int value); + + void setMinValue(int minValue); + void setMaxValue(int maxValue); + private: + void onEditTextChange(MyGUI::EditBox* sender); + void onKeyLostFocus(MyGUI::Widget* _new); + + int mValue; + + int mMinValue; + int mMaxValue; + }; + +} + +#endif diff --git a/components/widgets/sharedstatebutton.cpp b/components/widgets/sharedstatebutton.cpp new file mode 100644 index 000000000..6859a3065 --- /dev/null +++ b/components/widgets/sharedstatebutton.cpp @@ -0,0 +1,128 @@ +#include "sharedstatebutton.hpp" + +namespace Gui +{ + + SharedStateButton::SharedStateButton() + : mIsMousePressed(false) + , mIsMouseFocus(false) + { + + } + + void SharedStateButton::shutdownOverride() + { + ButtonGroup group = mSharedWith; // make a copy so that we don't nuke the vector during iteration + for (ButtonGroup::iterator it = group.begin(); it != group.end(); ++it) + { + (*it)->shareStateWith(ButtonGroup()); + } + } + + void SharedStateButton::shareStateWith(ButtonGroup shared) + { + mSharedWith = shared; + } + + void SharedStateButton::onMouseButtonPressed(int _left, int _top, MyGUI::MouseButton _id) + { + mIsMousePressed = true; + Base::onMouseButtonPressed(_left, _top, _id); + updateButtonState(); + } + + void SharedStateButton::onMouseButtonReleased(int _left, int _top, MyGUI::MouseButton _id) + { + mIsMousePressed = false; + Base::onMouseButtonReleased(_left, _top, _id); + updateButtonState(); + } + + void SharedStateButton::onMouseSetFocus(MyGUI::Widget *_old) + { + mIsMouseFocus = true; + Base::onMouseSetFocus(_old); + updateButtonState(); + } + + void SharedStateButton::onMouseLostFocus(MyGUI::Widget *_new) + { + mIsMouseFocus = false; + Base::onMouseLostFocus(_new); + updateButtonState(); + } + + void SharedStateButton::baseUpdateEnable() + { + Base::baseUpdateEnable(); + updateButtonState(); + } + + void SharedStateButton::setStateSelected(bool _value) + { + Base::setStateSelected(_value); + updateButtonState(); + + for (ButtonGroup::iterator it = mSharedWith.begin(); it != mSharedWith.end(); ++it) + { + (*it)->MyGUI::Button::setStateSelected(getStateSelected()); + } + } + + bool SharedStateButton::_setState(const std::string &_value) + { + bool ret = _setWidgetState(_value); + if (ret) + { + for (ButtonGroup::iterator it = mSharedWith.begin(); it != mSharedWith.end(); ++it) + { + (*it)->_setWidgetState(_value); + } + } + return ret; + } + + void SharedStateButton::updateButtonState() + { + if (getStateSelected()) + { + if (!getInheritedEnabled()) + { + if (!_setState("disabled_checked")) + _setState("disabled"); + } + else if (mIsMousePressed) + { + if (!_setState("pushed_checked")) + _setState("pushed"); + } + else if (mIsMouseFocus) + { + if (!_setState("highlighted_checked")) + _setState("pushed"); + } + else + _setState("normal_checked"); + } + else + { + if (!getInheritedEnabled()) + _setState("disabled"); + else if (mIsMousePressed) + _setState("pushed"); + else if (mIsMouseFocus) + _setState("highlighted"); + else + _setState("normal"); + } + } + + void SharedStateButton::createButtonGroup(ButtonGroup group) + { + for (ButtonGroup::iterator it = group.begin(); it != group.end(); ++it) + { + (*it)->shareStateWith(group); + } + } + +} diff --git a/components/widgets/sharedstatebutton.hpp b/components/widgets/sharedstatebutton.hpp new file mode 100644 index 000000000..3d7fbc84e --- /dev/null +++ b/components/widgets/sharedstatebutton.hpp @@ -0,0 +1,51 @@ +#ifndef OPENMW_WIDGETS_SHAREDSTATEBUTTON_HPP +#define OPENMW_WIDGETS_SHAREDSTATEBUTTON_HPP + +#include + +namespace Gui +{ + + class SharedStateButton; + + typedef std::vector ButtonGroup; + + /// @brief A button that applies its own state changes to other widgets, to do this you define it as part of a ButtonGroup. + class SharedStateButton : public MyGUI::Button + { + MYGUI_RTTI_DERIVED(SharedStateButton) + + public: + SharedStateButton(); + + protected: + void updateButtonState(); + + virtual void onMouseButtonPressed(int _left, int _top, MyGUI::MouseButton _id); + virtual void onMouseButtonReleased(int _left, int _top, MyGUI::MouseButton _id); + virtual void onMouseSetFocus(MyGUI::Widget* _old); + virtual void onMouseLostFocus(MyGUI::Widget* _new); + virtual void baseUpdateEnable(); + + virtual void shutdownOverride(); + + bool _setState(const std::string &_value); + + public: + void shareStateWith(ButtonGroup shared); + + /// @note The ButtonGroup connection will be destroyed when any widget in the group gets destroyed. + static void createButtonGroup(ButtonGroup group); + + //! Set button selected state + void setStateSelected(bool _value); + + private: + ButtonGroup mSharedWith; + + bool mIsMousePressed; + bool mIsMouseFocus; + }; +} + +#endif diff --git a/components/widgets/tags.cpp b/components/widgets/tags.cpp new file mode 100644 index 000000000..160698c8e --- /dev/null +++ b/components/widgets/tags.cpp @@ -0,0 +1,57 @@ +#include "tags.hpp" + +#include + +namespace Gui +{ + +bool replaceTag(const MyGUI::UString& tag, MyGUI::UString& out, const std::map& fallbackSettings) +{ + std::string fontcolour = "fontcolour="; + size_t fontcolourLength = fontcolour.length(); + + std::string fontcolourhtml = "fontcolourhtml="; + size_t fontcolourhtmlLength = fontcolourhtml.length(); + + if (tag.compare(0, fontcolourLength, fontcolour) == 0) + { + std::string fallbackName = "FontColor_color_" + tag.substr(fontcolourLength); + std::map::const_iterator it = fallbackSettings.find(fallbackName); + if (it == fallbackSettings.end()) + throw std::runtime_error("Unknown fallback name: " + fallbackName); + std::string str = it->second; + + std::string ret[3]; + unsigned int j=0; + for(unsigned int i=0;i::const_iterator it = fallbackSettings.find(fallbackName); + if (it == fallbackSettings.end()) + throw std::runtime_error("Unknown fallback name: " + fallbackName); + std::string str = it->second; + + std::string ret[3]; + unsigned int j=0; + for(unsigned int i=0;i +#include +#include + +namespace Gui +{ + +/// Try to replace a tag. Returns true on success and writes the result to \a out. +bool replaceTag (const MyGUI::UString& tag, MyGUI::UString& out, const std::map& fallbackSettings); + +} + +#endif diff --git a/components/widgets/widgets.cpp b/components/widgets/widgets.cpp new file mode 100644 index 000000000..3b6361cf8 --- /dev/null +++ b/components/widgets/widgets.cpp @@ -0,0 +1,29 @@ +#include "widgets.hpp" + +#include + +#include "list.hpp" +#include "numericeditbox.hpp" +#include "box.hpp" +#include "imagebutton.hpp" +#include "sharedstatebutton.hpp" +#include "windowcaption.hpp" + +namespace Gui +{ + + void registerAllWidgets() + { + MyGUI::FactoryManager::getInstance().registerFactory("Widget"); + MyGUI::FactoryManager::getInstance().registerFactory("Widget"); + MyGUI::FactoryManager::getInstance().registerFactory("Widget"); + MyGUI::FactoryManager::getInstance().registerFactory("Widget"); + MyGUI::FactoryManager::getInstance().registerFactory("Widget"); + MyGUI::FactoryManager::getInstance().registerFactory("Widget"); + MyGUI::FactoryManager::getInstance().registerFactory("Widget"); + MyGUI::FactoryManager::getInstance().registerFactory("Widget"); + MyGUI::FactoryManager::getInstance().registerFactory("Widget"); + MyGUI::FactoryManager::getInstance().registerFactory("Widget"); + } + +} diff --git a/components/widgets/widgets.hpp b/components/widgets/widgets.hpp new file mode 100644 index 000000000..d17132135 --- /dev/null +++ b/components/widgets/widgets.hpp @@ -0,0 +1,12 @@ +#ifndef OPENMW_COMPONENTS_WIDGETS_H +#define OPENMW_COMPONENTS_WIDGETS_H + +namespace Gui +{ + + /// Register all widgets from this component with MyGUI's factory manager. + void registerAllWidgets(); + +} + +#endif diff --git a/components/widgets/windowcaption.cpp b/components/widgets/windowcaption.cpp new file mode 100644 index 000000000..bcb0a7c12 --- /dev/null +++ b/components/widgets/windowcaption.cpp @@ -0,0 +1,58 @@ +#include "windowcaption.hpp" + +#include + +namespace Gui +{ + + WindowCaption::WindowCaption() + : mLeft(NULL) + , mRight(NULL) + { + } + + void WindowCaption::initialiseOverride() + { + Base::initialiseOverride(); + + assignWidget(mLeft, "Left"); + assignWidget(mRight, "Right"); + + assignWidget(mClient, "Client"); + if (!mClient) + throw std::runtime_error("WindowCaption needs an EditBox Client widget in its skin"); + } + + void WindowCaption::setCaption(const MyGUI::UString &_value) + { + EditBox::setCaption(_value); + align(); + } + + void WindowCaption::setSize(const MyGUI::IntSize& _value) + { + Base::setSize(_value); + align(); + } + + void WindowCaption::setCoord(const MyGUI::IntCoord& _value) + { + Base::setCoord(_value); + align(); + } + + void WindowCaption::align() + { + MyGUI::IntSize textSize = getTextSize(); + MyGUI::Widget* caption = mClient; + caption->setSize(textSize.width + 24, caption->getHeight()); + + int barwidth = (getWidth()-caption->getWidth())/2; + caption->setPosition(barwidth, caption->getTop()); + if (mLeft) + mLeft->setCoord(0, mLeft->getTop(), barwidth, mLeft->getHeight()); + if (mRight) + mRight->setCoord(barwidth + caption->getWidth(), mRight->getTop(), barwidth, mRight->getHeight()); + } + +} diff --git a/components/widgets/windowcaption.hpp b/components/widgets/windowcaption.hpp new file mode 100644 index 000000000..bdd4c0a2e --- /dev/null +++ b/components/widgets/windowcaption.hpp @@ -0,0 +1,32 @@ +#ifndef OPENMW_WIDGETS_WINDOWCAPTION_H +#define OPENMW_WIDGETS_WINDOWCAPTION_H + +#include + +namespace Gui +{ + + /// Window caption that automatically adjusts "Left" and "Right" widgets in its skin + /// based on the text size of the caption in the middle + class WindowCaption : public MyGUI::EditBox + { + MYGUI_RTTI_DERIVED(WindowCaption) + public: + WindowCaption(); + + virtual void setCaption(const MyGUI::UString &_value); + virtual void initialiseOverride(); + + virtual void setSize(const MyGUI::IntSize& _value); + virtual void setCoord(const MyGUI::IntCoord& _value); + + private: + MyGUI::Widget* mLeft; + MyGUI::Widget* mRight; + + void align(); + }; + +} + +#endif diff --git a/credits.txt b/credits.txt deleted file mode 100644 index 5c757f957..000000000 --- a/credits.txt +++ /dev/null @@ -1,171 +0,0 @@ -Contributors - -The OpenMW project was started in 2008 by Nicolay Korslund. -In the course of years many people have contributed to the project. - -If you feel your name is missing from this list, -please notify a developer. - - -Programmers: -Marc Zinnschlag (Zini) - Lead Programmer/Project Manager - -Adam Hogan (aurix) -Aleksandar Jovanov -Alex Haddad (rainChu) -Alex McKibben (WeirdSexy) -Alexander Nadeau (wareya) -Alexander Olofsson (Ace) -Artem Kotsynyak (greye) -Arthur Moore (EmperorArthur) -athile -Bret Curtis (psi29a) -Britt Mathis (galdor557) -cc9cii -Chris Boyce (slothlife) -Chris Robinson (KittyCat) -Cory F. Cohen (cfcohen) -Cris Mihalache (Mirceam) -darkf -Dmitry Shkurskiy (endorph) -Douglas Diniz (Dgdiniz) -Douglas Mencken (dougmencken) -Edmondo Tommasina (edmondo) -Eduard Cot (trombonecot) -Eli2 -Emanuel Guével (potatoesmaster) -Fil Krynicki (filkry) -gugus/gus -Hallfaer Tuilinn -Jacob Essex (Yacoby) -Jannik Heller (scrawl) -Jason Hooks (jhooks) -Jeffrey Haines (Jyby) -Joel Graff (graffy) -John Blomberg (fstp) -Jordan Milne -Julien Voisin (jvoisin/ap0) -Karl-Felix Glatzer (k1ll) -Lars Söderberg (Lazaroth) -lazydev -Leon Saunders (emoose) -Lukasz Gromanowski (lgro) -Manuel Edelmann (vorenon) -Marc Bouvier (CramitDeFrog) -Marcin Hulist (Gohan) -Mark Siewert (mark76) -Mateusz Kołaczek (PL_kolek) -megaton -Michael Hogan (Xethik) -Michael Mc Donnell -Michael Papageorgiou (werdanith) -Michał Bień (Glorf) -Nathan Jeffords (blunted2night) -Nikolay Kasyanov (corristo) -Nolan Poe (nopoe) -Paul McElroy (Greendogo) -Pieter van der Kloet (pvdk) -Radu-Marius Popovici (rpopovici) -Roman Melnik (Kromgart) -Roman Proskuryakov (humbug) -Sandy Carter (bwrsandman) -Sebastian Wick (swick) -Sergey Shambir -sir_herrbatka -Sylvain Thesnieres (Garvek) -Thomas Luppi (Digmaster) -Tom Mason (wheybags) -Torben Leif Carrington (TorbenC) - -Packagers: -Alexander Olofsson (Ace) - Windows -Bret Curtis (psi29a) - Ubuntu Linux -Edmondo Tommasina (edmondo) - Gentoo Linux -Julian Ospald (hasufell) - Gentoo Linux -Karl-Felix Glatzer (k1ll) - Linux Binaries -Kenny Armstrong (artorius) - Fedora Linux -Nikolay Kasyanov (corristo) - Mac OS X -Sandy Carter (bwrsandman) - Arch Linux - - -Public Relations and Translations: -Alex McKibben (WeirdSexy) - Podcaster -Artem Kotsynyak (greye) - Russian News Writer -Jim Clauwaert (Zedd) - Public Outreach -Julien Voisin (jvoisin/ap0) - French News Writer -Lukasz Gromanowski (lgro) - English News Writer -Mickey Lyle (raevol) - Release Manager -Pithorn - Chinese News Writer -sir_herrbatka - Polish News Writer - - -Website: -Lukasz Gromanowski (lgro) - Website Administrator -Ryan Sardonic (Wry) - Wiki Editor -sir_herrbatka - Forum Administrator - - -Formula Research: -Hrnchamd -Epsilon -fragonard -Greendogo -HiPhish -modred11 -Myckel -natirips -Sadler - - -Artwork: -Necrod - OpenMW Logo -Mickey Lyle (raevol) - Wordpress Theme -Okulo - OpenMW Editor Icons - -Inactive Contributors: -Ardekantur -Armin Preiml -Carl Maxwell -Diggory Hardy -Dmitry Marakasov (AMDmi3) -ElderTroll -guidoj -Jan-Peter Nilsson (peppe) -Jan Borsodi -Josua Grawitter -juanmnzsk8 -Kingpix -Lordrea -Michal Sciubidlo -Nicolay Korslund -pchan3 -penguinroad -psi29a -sergoz -spyboot -Star-Demon -Thoronador -Yuri Krupenin - - -Additional Credits: -In this section we would like to thank people not part of OpenMW for their work. - -Thanks to Maxim Nikolaev, -for allowing us to use his excellent Morrowind fan-art on our website and in other places. - -Thanks to DokterDume, -for kindly providing us with the Moon and Star logo, -used as the application icon and project logo. - -Thanks to Kevin Ryan, -for creating the icon used for the Data Files tab of the OpenMW Launcher. - -Thanks to Georg Duffner, -for his EB Garamond fontface, see OFL.txt for his license terms. - -Thanks to Dongle, -for his Daedric fontface, see Daedric Font License.txt for his license terms. - -Thanks to DejaVu team, -for their DejaVuLGCSansMono fontface, see DejaVu Font License.txt for their license terms. diff --git a/docs/Doxyfile b/docs/Doxyfile deleted file mode 100644 index 156e23abd..000000000 --- a/docs/Doxyfile +++ /dev/null @@ -1,1543 +0,0 @@ -# Doxyfile 1.5.8 - -# This file describes the settings to be used by the documentation system -# doxygen (www.doxygen.org) for a project -# -# All text after a hash (#) is considered a comment and will be ignored -# The format is: -# TAG = value [value, ...] -# For lists items can also be appended using: -# TAG += value [value, ...] -# Values that contain spaces should be placed between quotes (" ") - -#--------------------------------------------------------------------------- -# Project related configuration options -#--------------------------------------------------------------------------- - -# This tag specifies the encoding used for all characters in the config file -# that follow. The default is UTF-8 which is also the encoding used for all -# text before the first occurrence of this tag. Doxygen uses libiconv (or the -# iconv built into libc) for the transcoding. See -# http://www.gnu.org/software/libiconv for the list of possible encodings. - -DOXYFILE_ENCODING = UTF-8 - -# The PROJECT_NAME tag is a single word (or a sequence of words surrounded -# by quotes) that should identify the project. - -PROJECT_NAME = OpenMW - -# The PROJECT_NUMBER tag can be used to enter a project or revision number. -# This could be handy for archiving the generated documentation or -# if some version control system is used. - -PROJECT_NUMBER = - -# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) -# base path where the generated documentation will be put. -# If a relative path is entered, it will be relative to the location -# where doxygen was started. If left blank the current directory will be used. - -OUTPUT_DIRECTORY = Doxygen - -# If the CREATE_SUBDIRS tag is set to YES, then doxygen will create -# 4096 sub-directories (in 2 levels) under the output directory of each output -# format and will distribute the generated files over these directories. -# Enabling this option can be useful when feeding doxygen a huge amount of -# source files, where putting all generated files in the same directory would -# otherwise cause performance problems for the file system. - -CREATE_SUBDIRS = NO - -# The OUTPUT_LANGUAGE tag is used to specify the language in which all -# documentation generated by doxygen is written. Doxygen will use this -# information to generate all constant output in the proper language. -# The default language is English, other supported languages are: -# Afrikaans, Arabic, Brazilian, Catalan, Chinese, Chinese-Traditional, -# Croatian, Czech, Danish, Dutch, Farsi, Finnish, French, German, Greek, -# Hungarian, Italian, Japanese, Japanese-en (Japanese with English messages), -# Korean, Korean-en, Lithuanian, Norwegian, Macedonian, Persian, Polish, -# Portuguese, Romanian, Russian, Serbian, Serbian-Cyrilic, Slovak, Slovene, -# Spanish, Swedish, and Ukrainian. - -OUTPUT_LANGUAGE = English - -# If the BRIEF_MEMBER_DESC tag is set to YES (the default) Doxygen will -# include brief member descriptions after the members that are listed in -# the file and class documentation (similar to JavaDoc). -# Set to NO to disable this. - -BRIEF_MEMBER_DESC = YES - -# If the REPEAT_BRIEF tag is set to YES (the default) Doxygen will prepend -# the brief description of a member or function before the detailed description. -# Note: if both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the -# brief descriptions will be completely suppressed. - -REPEAT_BRIEF = YES - -# This tag implements a quasi-intelligent brief description abbreviator -# that is used to form the text in various listings. Each string -# in this list, if found as the leading text of the brief description, will be -# stripped from the text and the result after processing the whole list, is -# used as the annotated text. Otherwise, the brief description is used as-is. -# If left blank, the following values are used ("$name" is automatically -# replaced with the name of the entity): "The $name class" "The $name widget" -# "The $name file" "is" "provides" "specifies" "contains" -# "represents" "a" "an" "the" - -ABBREVIATE_BRIEF = "The $name class" \ - "The $name widget" \ - "The $name file" \ - is \ - provides \ - specifies \ - contains \ - represents \ - a \ - an \ - the - -# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then -# Doxygen will generate a detailed section even if there is only a brief -# description. - -ALWAYS_DETAILED_SEC = NO - -# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all -# inherited members of a class in the documentation of that class as if those -# members were ordinary class members. Constructors, destructors and assignment -# operators of the base classes will not be shown. - -INLINE_INHERITED_MEMB = NO - -# If the FULL_PATH_NAMES tag is set to YES then Doxygen will prepend the full -# path before files name in the file list and in the header files. If set -# to NO the shortest path that makes the file name unique will be used. - -FULL_PATH_NAMES = YES - -# If the FULL_PATH_NAMES tag is set to YES then the STRIP_FROM_PATH tag -# can be used to strip a user-defined part of the path. Stripping is -# only done if one of the specified strings matches the left-hand part of -# the path. The tag can be used to show relative paths in the file list. -# If left blank the directory from which doxygen is run is used as the -# path to strip. - -STRIP_FROM_PATH = - -# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of -# the path mentioned in the documentation of a class, which tells -# the reader which header file to include in order to use a class. -# If left blank only the name of the header file containing the class -# definition is used. Otherwise one should specify the include paths that -# are normally passed to the compiler using the -I flag. - -STRIP_FROM_INC_PATH = - -# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter -# (but less readable) file names. This can be useful is your file systems -# doesn't support long names like on DOS, Mac, or CD-ROM. - -SHORT_NAMES = NO - -# If the JAVADOC_AUTOBRIEF tag is set to YES then Doxygen -# will interpret the first line (until the first dot) of a JavaDoc-style -# comment as the brief description. If set to NO, the JavaDoc -# comments will behave just like regular Qt-style comments -# (thus requiring an explicit @brief command for a brief description.) - -JAVADOC_AUTOBRIEF = NO - -# If the QT_AUTOBRIEF tag is set to YES then Doxygen will -# interpret the first line (until the first dot) of a Qt-style -# comment as the brief description. If set to NO, the comments -# will behave just like regular Qt-style comments (thus requiring -# an explicit \brief command for a brief description.) - -QT_AUTOBRIEF = NO - -# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make Doxygen -# treat a multi-line C++ special comment block (i.e. a block of //! or /// -# comments) as a brief description. This used to be the default behaviour. -# The new default is to treat a multi-line C++ comment block as a detailed -# description. Set this tag to YES if you prefer the old behaviour instead. - -MULTILINE_CPP_IS_BRIEF = NO - -# If the INHERIT_DOCS tag is set to YES (the default) then an undocumented -# member inherits the documentation from any documented member that it -# re-implements. - -INHERIT_DOCS = YES - -# If the SEPARATE_MEMBER_PAGES tag is set to YES, then doxygen will produce -# a new page for each member. If set to NO, the documentation of a member will -# be part of the file/class/namespace that contains it. - -SEPARATE_MEMBER_PAGES = NO - -# The TAB_SIZE tag can be used to set the number of spaces in a tab. -# Doxygen uses this value to replace tabs by spaces in code fragments. - -TAB_SIZE = 8 - -# This tag can be used to specify a number of aliases that acts -# as commands in the documentation. An alias has the form "name=value". -# For example adding "sideeffect=\par Side Effects:\n" will allow you to -# put the command \sideeffect (or @sideeffect) in the documentation, which -# will result in a user-defined paragraph with heading "Side Effects:". -# You can put \n's in the value part of an alias to insert newlines. - -ALIASES = - -# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C -# sources only. Doxygen will then generate output that is more tailored for C. -# For instance, some of the names that are used will be different. The list -# of all members will be omitted, etc. - -OPTIMIZE_OUTPUT_FOR_C = NO - -# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java -# sources only. Doxygen will then generate output that is more tailored for -# Java. For instance, namespaces will be presented as packages, qualified -# scopes will look different, etc. - -OPTIMIZE_OUTPUT_JAVA = NO - -# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran -# sources only. Doxygen will then generate output that is more tailored for -# Fortran. - -OPTIMIZE_FOR_FORTRAN = NO - -# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL -# sources. Doxygen will then generate output that is tailored for -# VHDL. - -OPTIMIZE_OUTPUT_VHDL = NO - -# Doxygen selects the parser to use depending on the extension of the files it parses. -# With this tag you can assign which parser to use for a given extension. -# Doxygen has a built-in mapping, but you can override or extend it using this tag. -# The format is ext=language, where ext is a file extension, and language is one of -# the parsers supported by doxygen: IDL, Java, Javascript, C#, C, C++, D, PHP, -# Objective-C, Python, Fortran, VHDL, C, C++. For instance to make doxygen treat -# .inc files as Fortran files (default is PHP), and .f files as C (default is Fortran), -# use: inc=Fortran f=C - -EXTENSION_MAPPING = - -# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want -# to include (a tag file for) the STL sources as input, then you should -# set this tag to YES in order to let doxygen match functions declarations and -# definitions whose arguments contain STL classes (e.g. func(std::string); v.s. -# func(std::string) {}). This also make the inheritance and collaboration -# diagrams that involve STL classes more complete and accurate. - -BUILTIN_STL_SUPPORT = NO - -# If you use Microsoft's C++/CLI language, you should set this option to YES to -# enable parsing support. - -CPP_CLI_SUPPORT = NO - -# Set the SIP_SUPPORT tag to YES if your project consists of sip sources only. -# Doxygen will parse them like normal C++ but will assume all classes use public -# instead of private inheritance when no explicit protection keyword is present. - -SIP_SUPPORT = NO - -# For Microsoft's IDL there are propget and propput attributes to indicate getter -# and setter methods for a property. Setting this option to YES (the default) -# will make doxygen to replace the get and set methods by a property in the -# documentation. This will only work if the methods are indeed getting or -# setting a simple type. If this is not the case, or you want to show the -# methods anyway, you should set this option to NO. - -IDL_PROPERTY_SUPPORT = YES - -# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC -# tag is set to YES, then doxygen will reuse the documentation of the first -# member in the group (if any) for the other members of the group. By default -# all members of a group must be documented explicitly. - -DISTRIBUTE_GROUP_DOC = NO - -# Set the SUBGROUPING tag to YES (the default) to allow class member groups of -# the same type (for instance a group of public functions) to be put as a -# subgroup of that type (e.g. under the Public Functions section). Set it to -# NO to prevent subgrouping. Alternatively, this can be done per class using -# the \nosubgrouping command. - -SUBGROUPING = YES - -# When TYPEDEF_HIDES_STRUCT is enabled, a typedef of a struct, union, or enum -# is documented as struct, union, or enum with the name of the typedef. So -# typedef struct TypeS {} TypeT, will appear in the documentation as a struct -# with name TypeT. When disabled the typedef will appear as a member of a file, -# namespace, or class. And the struct will be named TypeS. This can typically -# be useful for C code in case the coding convention dictates that all compound -# types are typedef'ed and only the typedef is referenced, never the tag name. - -TYPEDEF_HIDES_STRUCT = NO - -# The SYMBOL_CACHE_SIZE determines the size of the internal cache use to -# determine which symbols to keep in memory and which to flush to disk. -# When the cache is full, less often used symbols will be written to disk. -# For small to medium size projects (<1000 input files) the default value is -# probably good enough. For larger projects a too small cache size can cause -# doxygen to be busy swapping symbols to and from disk most of the time -# causing a significant performance penality. -# If the system has enough physical memory increasing the cache will improve the -# performance by keeping more symbols in memory. Note that the value works on -# a logarithmic scale so increasing the size by one will rougly double the -# memory usage. The cache size is given by this formula: -# 2^(16+SYMBOL_CACHE_SIZE). The valid range is 0..9, the default is 0, -# corresponding to a cache size of 2^16 = 65536 symbols - -SYMBOL_CACHE_SIZE = 0 - -#--------------------------------------------------------------------------- -# Build related configuration options -#--------------------------------------------------------------------------- - -# If the EXTRACT_ALL tag is set to YES doxygen will assume all entities in -# documentation are documented, even if no documentation was available. -# Private class members and static file members will be hidden unless -# the EXTRACT_PRIVATE and EXTRACT_STATIC tags are set to YES - -EXTRACT_ALL = YES - -# If the EXTRACT_PRIVATE tag is set to YES all private members of a class -# will be included in the documentation. - -EXTRACT_PRIVATE = YES - -# If the EXTRACT_STATIC tag is set to YES all static members of a file -# will be included in the documentation. - -EXTRACT_STATIC = YES - -# If the EXTRACT_LOCAL_CLASSES tag is set to YES classes (and structs) -# defined locally in source files will be included in the documentation. -# If set to NO only classes defined in header files are included. - -EXTRACT_LOCAL_CLASSES = YES - -# This flag is only useful for Objective-C code. When set to YES local -# methods, which are defined in the implementation section but not in -# the interface are included in the documentation. -# If set to NO (the default) only methods in the interface are included. - -EXTRACT_LOCAL_METHODS = YES - -# If this flag is set to YES, the members of anonymous namespaces will be -# extracted and appear in the documentation as a namespace called -# 'anonymous_namespace{file}', where file will be replaced with the base -# name of the file that contains the anonymous namespace. By default -# anonymous namespace are hidden. - -EXTRACT_ANON_NSPACES = YES - -# If the HIDE_UNDOC_MEMBERS tag is set to YES, Doxygen will hide all -# undocumented members of documented classes, files or namespaces. -# If set to NO (the default) these members will be included in the -# various overviews, but no documentation section is generated. -# This option has no effect if EXTRACT_ALL is enabled. - -HIDE_UNDOC_MEMBERS = NO - -# If the HIDE_UNDOC_CLASSES tag is set to YES, Doxygen will hide all -# undocumented classes that are normally visible in the class hierarchy. -# If set to NO (the default) these classes will be included in the various -# overviews. This option has no effect if EXTRACT_ALL is enabled. - -HIDE_UNDOC_CLASSES = NO - -# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, Doxygen will hide all -# friend (class|struct|union) declarations. -# If set to NO (the default) these declarations will be included in the -# documentation. - -HIDE_FRIEND_COMPOUNDS = NO - -# If the HIDE_IN_BODY_DOCS tag is set to YES, Doxygen will hide any -# documentation blocks found inside the body of a function. -# If set to NO (the default) these blocks will be appended to the -# function's detailed documentation block. - -HIDE_IN_BODY_DOCS = NO - -# The INTERNAL_DOCS tag determines if documentation -# that is typed after a \internal command is included. If the tag is set -# to NO (the default) then the documentation will be excluded. -# Set it to YES to include the internal documentation. - -INTERNAL_DOCS = NO - -# If the CASE_SENSE_NAMES tag is set to NO then Doxygen will only generate -# file names in lower-case letters. If set to YES upper-case letters are also -# allowed. This is useful if you have classes or files whose names only differ -# in case and if your file system supports case sensitive file names. Windows -# and Mac users are advised to set this option to NO. - -CASE_SENSE_NAMES = YES - -# If the HIDE_SCOPE_NAMES tag is set to NO (the default) then Doxygen -# will show members with their full class and namespace scopes in the -# documentation. If set to YES the scope will be hidden. - -HIDE_SCOPE_NAMES = NO - -# If the SHOW_INCLUDE_FILES tag is set to YES (the default) then Doxygen -# will put a list of the files that are included by a file in the documentation -# of that file. - -SHOW_INCLUDE_FILES = YES - -# If the INLINE_INFO tag is set to YES (the default) then a tag [inline] -# is inserted in the documentation for inline members. - -INLINE_INFO = YES - -# If the SORT_MEMBER_DOCS tag is set to YES (the default) then doxygen -# will sort the (detailed) documentation of file and class members -# alphabetically by member name. If set to NO the members will appear in -# declaration order. - -SORT_MEMBER_DOCS = YES - -# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the -# brief documentation of file, namespace and class members alphabetically -# by member name. If set to NO (the default) the members will appear in -# declaration order. - -SORT_BRIEF_DOCS = NO - -# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the -# hierarchy of group names into alphabetical order. If set to NO (the default) -# the group names will appear in their defined order. - -SORT_GROUP_NAMES = NO - -# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be -# sorted by fully-qualified names, including namespaces. If set to -# NO (the default), the class list will be sorted only by class name, -# not including the namespace part. -# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. -# Note: This option applies only to the class list, not to the -# alphabetical list. - -SORT_BY_SCOPE_NAME = NO - -# The GENERATE_TODOLIST tag can be used to enable (YES) or -# disable (NO) the todo list. This list is created by putting \todo -# commands in the documentation. - -GENERATE_TODOLIST = YES - -# The GENERATE_TESTLIST tag can be used to enable (YES) or -# disable (NO) the test list. This list is created by putting \test -# commands in the documentation. - -GENERATE_TESTLIST = YES - -# The GENERATE_BUGLIST tag can be used to enable (YES) or -# disable (NO) the bug list. This list is created by putting \bug -# commands in the documentation. - -GENERATE_BUGLIST = YES - -# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or -# disable (NO) the deprecated list. This list is created by putting -# \deprecated commands in the documentation. - -GENERATE_DEPRECATEDLIST= YES - -# The ENABLED_SECTIONS tag can be used to enable conditional -# documentation sections, marked by \if sectionname ... \endif. - -ENABLED_SECTIONS = - -# The MAX_INITIALIZER_LINES tag determines the maximum number of lines -# the initial value of a variable or define consists of for it to appear in -# the documentation. If the initializer consists of more lines than specified -# here it will be hidden. Use a value of 0 to hide initializers completely. -# The appearance of the initializer of individual variables and defines in the -# documentation can be controlled using \showinitializer or \hideinitializer -# command in the documentation regardless of this setting. - -MAX_INITIALIZER_LINES = 30 - -# Set the SHOW_USED_FILES tag to NO to disable the list of files generated -# at the bottom of the documentation of classes and structs. If set to YES the -# list will mention the files that were used to generate the documentation. - -SHOW_USED_FILES = YES - -# If the sources in your project are distributed over multiple directories -# then setting the SHOW_DIRECTORIES tag to YES will show the directory hierarchy -# in the documentation. The default is NO. - -SHOW_DIRECTORIES = NO - -# Set the SHOW_FILES tag to NO to disable the generation of the Files page. -# This will remove the Files entry from the Quick Index and from the -# Folder Tree View (if specified). The default is YES. - -SHOW_FILES = YES - -# Set the SHOW_NAMESPACES tag to NO to disable the generation of the -# Namespaces page. This will remove the Namespaces entry from the Quick Index -# and from the Folder Tree View (if specified). The default is YES. - -SHOW_NAMESPACES = YES - -# The FILE_VERSION_FILTER tag can be used to specify a program or script that -# doxygen should invoke to get the current version for each file (typically from -# the version control system). Doxygen will invoke the program by executing (via -# popen()) the command , where is the value of -# the FILE_VERSION_FILTER tag, and is the name of an input file -# provided by doxygen. Whatever the program writes to standard output -# is used as the file version. See the manual for examples. - -FILE_VERSION_FILTER = - -# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed by -# doxygen. The layout file controls the global structure of the generated output files -# in an output format independent way. The create the layout file that represents -# doxygen's defaults, run doxygen with the -l option. You can optionally specify a -# file name after the option, if omitted DoxygenLayout.xml will be used as the name -# of the layout file. - -LAYOUT_FILE = - -#--------------------------------------------------------------------------- -# configuration options related to warning and progress messages -#--------------------------------------------------------------------------- - -# The QUIET tag can be used to turn on/off the messages that are generated -# by doxygen. Possible values are YES and NO. If left blank NO is used. - -QUIET = NO - -# The WARNINGS tag can be used to turn on/off the warning messages that are -# generated by doxygen. Possible values are YES and NO. If left blank -# NO is used. - -WARNINGS = YES - -# If WARN_IF_UNDOCUMENTED is set to YES, then doxygen will generate warnings -# for undocumented members. If EXTRACT_ALL is set to YES then this flag will -# automatically be disabled. - -WARN_IF_UNDOCUMENTED = NO - -# If WARN_IF_DOC_ERROR is set to YES, doxygen will generate warnings for -# potential errors in the documentation, such as not documenting some -# parameters in a documented function, or documenting parameters that -# don't exist or using markup commands wrongly. - -WARN_IF_DOC_ERROR = YES - -# This WARN_NO_PARAMDOC option can be abled to get warnings for -# functions that are documented, but have no documentation for their parameters -# or return value. If set to NO (the default) doxygen will only warn about -# wrong or incomplete parameter documentation, but not about the absence of -# documentation. - -WARN_NO_PARAMDOC = NO - -# The WARN_FORMAT tag determines the format of the warning messages that -# doxygen can produce. The string should contain the $file, $line, and $text -# tags, which will be replaced by the file and line number from which the -# warning originated and the warning text. Optionally the format may contain -# $version, which will be replaced by the version of the file (if it could -# be obtained via FILE_VERSION_FILTER) - -WARN_FORMAT = "$file:$line: $text" - -# The WARN_LOGFILE tag can be used to specify a file to which warning -# and error messages should be written. If left blank the output is written -# to stderr. - -WARN_LOGFILE = - -#--------------------------------------------------------------------------- -# configuration options related to the input files -#--------------------------------------------------------------------------- - -# The INPUT tag can be used to specify the files and/or directories that contain -# documented source files. You may enter file names like "myfile.cpp" or -# directories like "/usr/src/myproject". Separate the files or directories -# with spaces. - -INPUT = apps \ - components \ - libs \ - docs - -# This tag can be used to specify the character encoding of the source files -# that doxygen parses. Internally doxygen uses the UTF-8 encoding, which is -# also the default input encoding. Doxygen uses libiconv (or the iconv built -# into libc) for the transcoding. See http://www.gnu.org/software/libiconv for -# the list of possible encodings. - -INPUT_ENCODING = UTF-8 - -# If the value of the INPUT tag contains directories, you can use the -# FILE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp -# and *.h) to filter out the source-files in the directories. If left -# blank the following patterns are tested: -# *.c *.cc *.cxx *.cpp *.c++ *.java *.ii *.ixx *.ipp *.i++ *.inl *.h *.hh *.hxx -# *.hpp *.h++ *.idl *.odl *.cs *.php *.php3 *.inc *.m *.mm *.py *.f90 - -FILE_PATTERNS = *.c \ - *.cc \ - *.cxx \ - *.cpp \ - *.c++ \ - *.java \ - *.ii \ - *.ixx \ - *.ipp \ - *.i++ \ - *.inl \ - *.h \ - *.hh \ - *.hxx \ - *.hpp \ - *.h++ \ - *.idl \ - *.odl \ - *.cs \ - *.php \ - *.php3 \ - *.inc \ - *.m \ - *.mm \ - *.dox \ - *.py \ - *.f90 \ - *.f \ - *.vhd \ - *.vhdl - -# The RECURSIVE tag can be used to turn specify whether or not subdirectories -# should be searched for input files as well. Possible values are YES and NO. -# If left blank NO is used. - -RECURSIVE = YES - -# The EXCLUDE tag can be used to specify files and/or directories that should -# excluded from the INPUT source files. This way you can easily exclude a -# subdirectory from a directory tree whose root is specified with the INPUT tag. - -EXCLUDE = - -# The EXCLUDE_SYMLINKS tag can be used select whether or not files or -# directories that are symbolic links (a Unix filesystem feature) are excluded -# from the input. - -EXCLUDE_SYMLINKS = NO - -# If the value of the INPUT tag contains directories, you can use the -# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude -# certain files from those directories. Note that the wildcards are matched -# against the file with absolute path, so to exclude all test directories -# for example use the pattern */test/* - -EXCLUDE_PATTERNS = - -# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names -# (namespaces, classes, functions, etc.) that should be excluded from the -# output. The symbol name can be a fully qualified name, a word, or if the -# wildcard * is used, a substring. Examples: ANamespace, AClass, -# AClass::ANamespace, ANamespace::*Test - -EXCLUDE_SYMBOLS = - -# The EXAMPLE_PATH tag can be used to specify one or more files or -# directories that contain example code fragments that are included (see -# the \include command). - -EXAMPLE_PATH = - -# If the value of the EXAMPLE_PATH tag contains directories, you can use the -# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp -# and *.h) to filter out the source-files in the directories. If left -# blank all files are included. - -EXAMPLE_PATTERNS = * - -# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be -# searched for input files to be used with the \include or \dontinclude -# commands irrespective of the value of the RECURSIVE tag. -# Possible values are YES and NO. If left blank NO is used. - -EXAMPLE_RECURSIVE = NO - -# The IMAGE_PATH tag can be used to specify one or more files or -# directories that contain image that are included in the documentation (see -# the \image command). - -IMAGE_PATH = - -# The INPUT_FILTER tag can be used to specify a program that doxygen should -# invoke to filter for each input file. Doxygen will invoke the filter program -# by executing (via popen()) the command , where -# is the value of the INPUT_FILTER tag, and is the name of an -# input file. Doxygen will then use the output that the filter program writes -# to standard output. If FILTER_PATTERNS is specified, this tag will be -# ignored. - -INPUT_FILTER = - -# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern -# basis. Doxygen will compare the file name with each pattern and apply the -# filter if there is a match. The filters are a list of the form: -# pattern=filter (like *.cpp=my_cpp_filter). See INPUT_FILTER for further -# info on how filters are used. If FILTER_PATTERNS is empty, INPUT_FILTER -# is applied to all files. - -FILTER_PATTERNS = - -# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using -# INPUT_FILTER) will be used to filter the input files when producing source -# files to browse (i.e. when SOURCE_BROWSER is set to YES). - -FILTER_SOURCE_FILES = NO - -#--------------------------------------------------------------------------- -# configuration options related to source browsing -#--------------------------------------------------------------------------- - -# If the SOURCE_BROWSER tag is set to YES then a list of source files will -# be generated. Documented entities will be cross-referenced with these sources. -# Note: To get rid of all source code in the generated output, make sure also -# VERBATIM_HEADERS is set to NO. - -SOURCE_BROWSER = NO - -# Setting the INLINE_SOURCES tag to YES will include the body -# of functions and classes directly in the documentation. - -INLINE_SOURCES = NO - -# Setting the STRIP_CODE_COMMENTS tag to YES (the default) will instruct -# doxygen to hide any special comment blocks from generated source code -# fragments. Normal C and C++ comments will always remain visible. - -STRIP_CODE_COMMENTS = YES - -# If the REFERENCED_BY_RELATION tag is set to YES -# then for each documented function all documented -# functions referencing it will be listed. - -REFERENCED_BY_RELATION = NO - -# If the REFERENCES_RELATION tag is set to YES -# then for each documented function all documented entities -# called/used by that function will be listed. - -REFERENCES_RELATION = NO - -# If the REFERENCES_LINK_SOURCE tag is set to YES (the default) -# and SOURCE_BROWSER tag is set to YES, then the hyperlinks from -# functions in REFERENCES_RELATION and REFERENCED_BY_RELATION lists will -# link to the source code. Otherwise they will link to the documentation. - -REFERENCES_LINK_SOURCE = YES - -# If the USE_HTAGS tag is set to YES then the references to source code -# will point to the HTML generated by the htags(1) tool instead of doxygen -# built-in source browser. The htags tool is part of GNU's global source -# tagging system (see http://www.gnu.org/software/global/global.html). You -# will need version 4.8.6 or higher. - -USE_HTAGS = NO - -# If the VERBATIM_HEADERS tag is set to YES (the default) then Doxygen -# will generate a verbatim copy of the header file for each class for -# which an include is specified. Set to NO to disable this. - -VERBATIM_HEADERS = YES - -#--------------------------------------------------------------------------- -# configuration options related to the alphabetical class index -#--------------------------------------------------------------------------- - -# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index -# of all compounds will be generated. Enable this if the project -# contains a lot of classes, structs, unions or interfaces. - -ALPHABETICAL_INDEX = NO - -# If the alphabetical index is enabled (see ALPHABETICAL_INDEX) then -# the COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns -# in which this list will be split (can be a number in the range [1..20]) - -COLS_IN_ALPHA_INDEX = 5 - -# In case all classes in a project start with a common prefix, all -# classes will be put under the same header in the alphabetical index. -# The IGNORE_PREFIX tag can be used to specify one or more prefixes that -# should be ignored while generating the index headers. - -IGNORE_PREFIX = - -#--------------------------------------------------------------------------- -# configuration options related to the HTML output -#--------------------------------------------------------------------------- - -# If the GENERATE_HTML tag is set to YES (the default) Doxygen will -# generate HTML output. - -GENERATE_HTML = YES - -# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. -# If a relative path is entered the value of OUTPUT_DIRECTORY will be -# put in front of it. If left blank `html' will be used as the default path. - -HTML_OUTPUT = html - -# The HTML_FILE_EXTENSION tag can be used to specify the file extension for -# each generated HTML page (for example: .htm,.php,.asp). If it is left blank -# doxygen will generate files with .html extension. - -HTML_FILE_EXTENSION = .html - -# The HTML_HEADER tag can be used to specify a personal HTML header for -# each generated HTML page. If it is left blank doxygen will generate a -# standard header. - -HTML_HEADER = - -# The HTML_FOOTER tag can be used to specify a personal HTML footer for -# each generated HTML page. If it is left blank doxygen will generate a -# standard footer. - -HTML_FOOTER = - -# The HTML_STYLESHEET tag can be used to specify a user-defined cascading -# style sheet that is used by each HTML page. It can be used to -# fine-tune the look of the HTML output. If the tag is left blank doxygen -# will generate a default style sheet. Note that doxygen will try to copy -# the style sheet file to the HTML output directory, so don't put your own -# stylesheet in the HTML output directory as well, or it will be erased! - -HTML_STYLESHEET = - -# If the HTML_ALIGN_MEMBERS tag is set to YES, the members of classes, -# files or namespaces will be aligned in HTML using tables. If set to -# NO a bullet list will be used. - -HTML_ALIGN_MEMBERS = YES - -# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML -# documentation will contain sections that can be hidden and shown after the -# page has loaded. For this to work a browser that supports -# JavaScript and DHTML is required (for instance Mozilla 1.0+, Firefox -# Netscape 6.0+, Internet explorer 5.0+, Konqueror, or Safari). - -HTML_DYNAMIC_SECTIONS = NO - -# If the GENERATE_DOCSET tag is set to YES, additional index files -# will be generated that can be used as input for Apple's Xcode 3 -# integrated development environment, introduced with OSX 10.5 (Leopard). -# To create a documentation set, doxygen will generate a Makefile in the -# HTML output directory. Running make will produce the docset in that -# directory and running "make install" will install the docset in -# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find -# it at startup. -# See http://developer.apple.com/tools/creatingdocsetswithdoxygen.html for more information. - -GENERATE_DOCSET = NO - -# When GENERATE_DOCSET tag is set to YES, this tag determines the name of the -# feed. A documentation feed provides an umbrella under which multiple -# documentation sets from a single provider (such as a company or product suite) -# can be grouped. - -DOCSET_FEEDNAME = "Doxygen generated docs" - -# When GENERATE_DOCSET tag is set to YES, this tag specifies a string that -# should uniquely identify the documentation set bundle. This should be a -# reverse domain-name style string, e.g. com.mycompany.MyDocSet. Doxygen -# will append .docset to the name. - -DOCSET_BUNDLE_ID = org.doxygen.Project - -# If the GENERATE_HTMLHELP tag is set to YES, additional index files -# will be generated that can be used as input for tools like the -# Microsoft HTML help workshop to generate a compiled HTML help file (.chm) -# of the generated HTML documentation. - -GENERATE_HTMLHELP = NO - -# If the GENERATE_HTMLHELP tag is set to YES, the CHM_FILE tag can -# be used to specify the file name of the resulting .chm file. You -# can add a path in front of the file if the result should not be -# written to the html output directory. - -CHM_FILE = - -# If the GENERATE_HTMLHELP tag is set to YES, the HHC_LOCATION tag can -# be used to specify the location (absolute path including file name) of -# the HTML help compiler (hhc.exe). If non-empty doxygen will try to run -# the HTML help compiler on the generated index.hhp. - -HHC_LOCATION = - -# If the GENERATE_HTMLHELP tag is set to YES, the GENERATE_CHI flag -# controls if a separate .chi index file is generated (YES) or that -# it should be included in the master .chm file (NO). - -GENERATE_CHI = NO - -# If the GENERATE_HTMLHELP tag is set to YES, the CHM_INDEX_ENCODING -# is used to encode HtmlHelp index (hhk), content (hhc) and project file -# content. - -CHM_INDEX_ENCODING = - -# If the GENERATE_HTMLHELP tag is set to YES, the BINARY_TOC flag -# controls whether a binary table of contents is generated (YES) or a -# normal table of contents (NO) in the .chm file. - -BINARY_TOC = NO - -# The TOC_EXPAND flag can be set to YES to add extra items for group members -# to the contents of the HTML help documentation and to the tree view. - -TOC_EXPAND = NO - -# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and QHP_VIRTUAL_FOLDER -# are set, an additional index file will be generated that can be used as input for -# Qt's qhelpgenerator to generate a Qt Compressed Help (.qch) of the generated -# HTML documentation. - -GENERATE_QHP = NO - -# If the QHG_LOCATION tag is specified, the QCH_FILE tag can -# be used to specify the file name of the resulting .qch file. -# The path specified is relative to the HTML output folder. - -QCH_FILE = - -# The QHP_NAMESPACE tag specifies the namespace to use when generating -# Qt Help Project output. For more information please see -# http://doc.trolltech.com/qthelpproject.html#namespace - -QHP_NAMESPACE = - -# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating -# Qt Help Project output. For more information please see -# http://doc.trolltech.com/qthelpproject.html#virtual-folders - -QHP_VIRTUAL_FOLDER = doc - -# If QHP_CUST_FILTER_NAME is set, it specifies the name of a custom filter to add. -# For more information please see -# http://doc.trolltech.com/qthelpproject.html#custom-filters - -QHP_CUST_FILTER_NAME = - -# The QHP_CUST_FILT_ATTRS tag specifies the list of the attributes of the custom filter to add.For more information please see -# Qt Help Project / Custom Filters. - -QHP_CUST_FILTER_ATTRS = - -# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this project's -# filter section matches. -# Qt Help Project / Filter Attributes. - -QHP_SECT_FILTER_ATTRS = - -# If the GENERATE_QHP tag is set to YES, the QHG_LOCATION tag can -# be used to specify the location of Qt's qhelpgenerator. -# If non-empty doxygen will try to run qhelpgenerator on the generated -# .qhp file. - -QHG_LOCATION = - -# The DISABLE_INDEX tag can be used to turn on/off the condensed index at -# top of each HTML page. The value NO (the default) enables the index and -# the value YES disables it. - -DISABLE_INDEX = NO - -# This tag can be used to set the number of enum values (range [1..20]) -# that doxygen will group on one line in the generated HTML documentation. - -ENUM_VALUES_PER_LINE = 4 - -# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index -# structure should be generated to display hierarchical information. -# If the tag value is set to FRAME, a side panel will be generated -# containing a tree-like index structure (just like the one that -# is generated for HTML Help). For this to work a browser that supports -# JavaScript, DHTML, CSS and frames is required (for instance Mozilla 1.0+, -# Netscape 6.0+, Internet explorer 5.0+, or Konqueror). Windows users are -# probably better off using the HTML help feature. Other possible values -# for this tag are: HIERARCHIES, which will generate the Groups, Directories, -# and Class Hierarchy pages using a tree view instead of an ordered list; -# ALL, which combines the behavior of FRAME and HIERARCHIES; and NONE, which -# disables this behavior completely. For backwards compatibility with previous -# releases of Doxygen, the values YES and NO are equivalent to FRAME and NONE -# respectively. - -GENERATE_TREEVIEW = NONE - -# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be -# used to set the initial width (in pixels) of the frame in which the tree -# is shown. - -TREEVIEW_WIDTH = 250 - -# Use this tag to change the font size of Latex formulas included -# as images in the HTML documentation. The default is 10. Note that -# when you change the font size after a successful doxygen run you need -# to manually remove any form_*.png images from the HTML output directory -# to force them to be regenerated. - -FORMULA_FONTSIZE = 10 - -#--------------------------------------------------------------------------- -# configuration options related to the LaTeX output -#--------------------------------------------------------------------------- - -# If the GENERATE_LATEX tag is set to YES (the default) Doxygen will -# generate Latex output. - -GENERATE_LATEX = NO - -# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. -# If a relative path is entered the value of OUTPUT_DIRECTORY will be -# put in front of it. If left blank `latex' will be used as the default path. - -LATEX_OUTPUT = latex - -# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be -# invoked. If left blank `latex' will be used as the default command name. - -LATEX_CMD_NAME = latex - -# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to -# generate index for LaTeX. If left blank `makeindex' will be used as the -# default command name. - -MAKEINDEX_CMD_NAME = makeindex - -# If the COMPACT_LATEX tag is set to YES Doxygen generates more compact -# LaTeX documents. This may be useful for small projects and may help to -# save some trees in general. - -COMPACT_LATEX = NO - -# The PAPER_TYPE tag can be used to set the paper type that is used -# by the printer. Possible values are: a4, a4wide, letter, legal and -# executive. If left blank a4wide will be used. - -PAPER_TYPE = a4wide - -# The EXTRA_PACKAGES tag can be to specify one or more names of LaTeX -# packages that should be included in the LaTeX output. - -EXTRA_PACKAGES = - -# The LATEX_HEADER tag can be used to specify a personal LaTeX header for -# the generated latex document. The header should contain everything until -# the first chapter. If it is left blank doxygen will generate a -# standard header. Notice: only use this tag if you know what you are doing! - -LATEX_HEADER = - -# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated -# is prepared for conversion to pdf (using ps2pdf). The pdf file will -# contain links (just like the HTML output) instead of page references -# This makes the output suitable for online browsing using a pdf viewer. - -PDF_HYPERLINKS = YES - -# If the USE_PDFLATEX tag is set to YES, pdflatex will be used instead of -# plain latex in the generated Makefile. Set this option to YES to get a -# higher quality PDF documentation. - -USE_PDFLATEX = YES - -# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \\batchmode. -# command to the generated LaTeX files. This will instruct LaTeX to keep -# running if errors occur, instead of asking the user for help. -# This option is also used when generating formulas in HTML. - -LATEX_BATCHMODE = NO - -# If LATEX_HIDE_INDICES is set to YES then doxygen will not -# include the index chapters (such as File Index, Compound Index, etc.) -# in the output. - -LATEX_HIDE_INDICES = NO - -#--------------------------------------------------------------------------- -# configuration options related to the RTF output -#--------------------------------------------------------------------------- - -# If the GENERATE_RTF tag is set to YES Doxygen will generate RTF output -# The RTF output is optimized for Word 97 and may not look very pretty with -# other RTF readers or editors. - -GENERATE_RTF = NO - -# The RTF_OUTPUT tag is used to specify where the RTF docs will be put. -# If a relative path is entered the value of OUTPUT_DIRECTORY will be -# put in front of it. If left blank `rtf' will be used as the default path. - -RTF_OUTPUT = rtf - -# If the COMPACT_RTF tag is set to YES Doxygen generates more compact -# RTF documents. This may be useful for small projects and may help to -# save some trees in general. - -COMPACT_RTF = NO - -# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated -# will contain hyperlink fields. The RTF file will -# contain links (just like the HTML output) instead of page references. -# This makes the output suitable for online browsing using WORD or other -# programs which support those fields. -# Note: wordpad (write) and others do not support links. - -RTF_HYPERLINKS = NO - -# Load stylesheet definitions from file. Syntax is similar to doxygen's -# config file, i.e. a series of assignments. You only have to provide -# replacements, missing definitions are set to their default value. - -RTF_STYLESHEET_FILE = - -# Set optional variables used in the generation of an rtf document. -# Syntax is similar to doxygen's config file. - -RTF_EXTENSIONS_FILE = - -#--------------------------------------------------------------------------- -# configuration options related to the man page output -#--------------------------------------------------------------------------- - -# If the GENERATE_MAN tag is set to YES (the default) Doxygen will -# generate man pages - -GENERATE_MAN = NO - -# The MAN_OUTPUT tag is used to specify where the man pages will be put. -# If a relative path is entered the value of OUTPUT_DIRECTORY will be -# put in front of it. If left blank `man' will be used as the default path. - -MAN_OUTPUT = man - -# The MAN_EXTENSION tag determines the extension that is added to -# the generated man pages (default is the subroutine's section .3) - -MAN_EXTENSION = .3 - -# If the MAN_LINKS tag is set to YES and Doxygen generates man output, -# then it will generate one additional man file for each entity -# documented in the real man page(s). These additional files -# only source the real man page, but without them the man command -# would be unable to find the correct page. The default is NO. - -MAN_LINKS = NO - -#--------------------------------------------------------------------------- -# configuration options related to the XML output -#--------------------------------------------------------------------------- - -# If the GENERATE_XML tag is set to YES Doxygen will -# generate an XML file that captures the structure of -# the code including all documentation. - -GENERATE_XML = NO - -# The XML_OUTPUT tag is used to specify where the XML pages will be put. -# If a relative path is entered the value of OUTPUT_DIRECTORY will be -# put in front of it. If left blank `xml' will be used as the default path. - -XML_OUTPUT = xml - -# The XML_SCHEMA tag can be used to specify an XML schema, -# which can be used by a validating XML parser to check the -# syntax of the XML files. - -XML_SCHEMA = - -# The XML_DTD tag can be used to specify an XML DTD, -# which can be used by a validating XML parser to check the -# syntax of the XML files. - -XML_DTD = - -# If the XML_PROGRAMLISTING tag is set to YES Doxygen will -# dump the program listings (including syntax highlighting -# and cross-referencing information) to the XML output. Note that -# enabling this will significantly increase the size of the XML output. - -XML_PROGRAMLISTING = YES - -#--------------------------------------------------------------------------- -# configuration options for the AutoGen Definitions output -#--------------------------------------------------------------------------- - -# If the GENERATE_AUTOGEN_DEF tag is set to YES Doxygen will -# generate an AutoGen Definitions (see autogen.sf.net) file -# that captures the structure of the code including all -# documentation. Note that this feature is still experimental -# and incomplete at the moment. - -GENERATE_AUTOGEN_DEF = NO - -#--------------------------------------------------------------------------- -# configuration options related to the Perl module output -#--------------------------------------------------------------------------- - -# If the GENERATE_PERLMOD tag is set to YES Doxygen will -# generate a Perl module file that captures the structure of -# the code including all documentation. Note that this -# feature is still experimental and incomplete at the -# moment. - -GENERATE_PERLMOD = NO - -# If the PERLMOD_LATEX tag is set to YES Doxygen will generate -# the necessary Makefile rules, Perl scripts and LaTeX code to be able -# to generate PDF and DVI output from the Perl module output. - -PERLMOD_LATEX = NO - -# If the PERLMOD_PRETTY tag is set to YES the Perl module output will be -# nicely formatted so it can be parsed by a human reader. This is useful -# if you want to understand what is going on. On the other hand, if this -# tag is set to NO the size of the Perl module output will be much smaller -# and Perl will parse it just the same. - -PERLMOD_PRETTY = YES - -# The names of the make variables in the generated doxyrules.make file -# are prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. -# This is useful so different doxyrules.make files included by the same -# Makefile don't overwrite each other's variables. - -PERLMOD_MAKEVAR_PREFIX = - -#--------------------------------------------------------------------------- -# Configuration options related to the preprocessor -#--------------------------------------------------------------------------- - -# If the ENABLE_PREPROCESSING tag is set to YES (the default) Doxygen will -# evaluate all C-preprocessor directives found in the sources and include -# files. - -ENABLE_PREPROCESSING = YES - -# If the MACRO_EXPANSION tag is set to YES Doxygen will expand all macro -# names in the source code. If set to NO (the default) only conditional -# compilation will be performed. Macro expansion can be done in a controlled -# way by setting EXPAND_ONLY_PREDEF to YES. - -MACRO_EXPANSION = NO - -# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES -# then the macro expansion is limited to the macros specified with the -# PREDEFINED and EXPAND_AS_DEFINED tags. - -EXPAND_ONLY_PREDEF = NO - -# If the SEARCH_INCLUDES tag is set to YES (the default) the includes files -# in the INCLUDE_PATH (see below) will be search if a #include is found. - -SEARCH_INCLUDES = YES - -# The INCLUDE_PATH tag can be used to specify one or more directories that -# contain include files that are not input files but should be processed by -# the preprocessor. - -INCLUDE_PATH = - -# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard -# patterns (like *.h and *.hpp) to filter out the header-files in the -# directories. If left blank, the patterns specified with FILE_PATTERNS will -# be used. - -INCLUDE_FILE_PATTERNS = - -# The PREDEFINED tag can be used to specify one or more macro names that -# are defined before the preprocessor is started (similar to the -D option of -# gcc). The argument of the tag is a list of macros of the form: name -# or name=definition (no spaces). If the definition and the = are -# omitted =1 is assumed. To prevent a macro definition from being -# undefined via #undef or recursively expanded use the := operator -# instead of the = operator. - -PREDEFINED = - -# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then -# this tag can be used to specify a list of macro names that should be expanded. -# The macro definition that is found in the sources will be used. -# Use the PREDEFINED tag if you want to use a different macro definition. - -EXPAND_AS_DEFINED = - -# If the SKIP_FUNCTION_MACROS tag is set to YES (the default) then -# doxygen's preprocessor will remove all function-like macros that are alone -# on a line, have an all uppercase name, and do not end with a semicolon. Such -# function macros are typically used for boiler-plate code, and will confuse -# the parser if not removed. - -SKIP_FUNCTION_MACROS = YES - -#--------------------------------------------------------------------------- -# Configuration::additions related to external references -#--------------------------------------------------------------------------- - -# The TAGFILES option can be used to specify one or more tagfiles. -# Optionally an initial location of the external documentation -# can be added for each tagfile. The format of a tag file without -# this location is as follows: -# TAGFILES = file1 file2 ... -# Adding location for the tag files is done as follows: -# TAGFILES = file1=loc1 "file2 = loc2" ... -# where "loc1" and "loc2" can be relative or absolute paths or -# URLs. If a location is present for each tag, the installdox tool -# does not have to be run to correct the links. -# Note that each tag file must have a unique name -# (where the name does NOT include the path) -# If a tag file is not located in the directory in which doxygen -# is run, you must also specify the path to the tagfile here. - -TAGFILES = - -# When a file name is specified after GENERATE_TAGFILE, doxygen will create -# a tag file that is based on the input files it reads. - -GENERATE_TAGFILE = - -# If the ALLEXTERNALS tag is set to YES all external classes will be listed -# in the class index. If set to NO only the inherited external classes -# will be listed. - -ALLEXTERNALS = NO - -# If the EXTERNAL_GROUPS tag is set to YES all external groups will be listed -# in the modules index. If set to NO, only the current project's groups will -# be listed. - -EXTERNAL_GROUPS = YES - -# The PERL_PATH should be the absolute path and name of the perl script -# interpreter (i.e. the result of `which perl'). - -PERL_PATH = /usr/bin/perl - -#--------------------------------------------------------------------------- -# Configuration options related to the dot tool -#--------------------------------------------------------------------------- - -# If the CLASS_DIAGRAMS tag is set to YES (the default) Doxygen will -# generate a inheritance diagram (in HTML, RTF and LaTeX) for classes with base -# or super classes. Setting the tag to NO turns the diagrams off. Note that -# this option is superseded by the HAVE_DOT option below. This is only a -# fallback. It is recommended to install and use dot, since it yields more -# powerful graphs. - -CLASS_DIAGRAMS = YES - -# You can define message sequence charts within doxygen comments using the \msc -# command. Doxygen will then run the mscgen tool (see -# http://www.mcternan.me.uk/mscgen/) to produce the chart and insert it in the -# documentation. The MSCGEN_PATH tag allows you to specify the directory where -# the mscgen tool resides. If left empty the tool is assumed to be found in the -# default search path. - -MSCGEN_PATH = - -# If set to YES, the inheritance and collaboration graphs will hide -# inheritance and usage relations if the target is undocumented -# or is not a class. - -HIDE_UNDOC_RELATIONS = YES - -# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is -# available from the path. This tool is part of Graphviz, a graph visualization -# toolkit from AT&T and Lucent Bell Labs. The other options in this section -# have no effect if this option is set to NO (the default) - -HAVE_DOT = YES - -# By default doxygen will write a font called FreeSans.ttf to the output -# directory and reference it in all dot files that doxygen generates. This -# font does not include all possible unicode characters however, so when you need -# these (or just want a differently looking font) you can specify the font name -# using DOT_FONTNAME. You need need to make sure dot is able to find the font, -# which can be done by putting it in a standard location or by setting the -# DOTFONTPATH environment variable or by setting DOT_FONTPATH to the directory -# containing the font. - -DOT_FONTNAME = FreeSans - -# The DOT_FONTSIZE tag can be used to set the size of the font of dot graphs. -# The default size is 10pt. - -DOT_FONTSIZE = 10 - -# By default doxygen will tell dot to use the output directory to look for the -# FreeSans.ttf font (which doxygen will put there itself). If you specify a -# different font using DOT_FONTNAME you can set the path where dot -# can find it using this tag. - -DOT_FONTPATH = - -# If the CLASS_GRAPH and HAVE_DOT tags are set to YES then doxygen -# will generate a graph for each documented class showing the direct and -# indirect inheritance relations. Setting this tag to YES will force the -# the CLASS_DIAGRAMS tag to NO. - -CLASS_GRAPH = YES - -# If the COLLABORATION_GRAPH and HAVE_DOT tags are set to YES then doxygen -# will generate a graph for each documented class showing the direct and -# indirect implementation dependencies (inheritance, containment, and -# class references variables) of the class with other documented classes. - -COLLABORATION_GRAPH = YES - -# If the GROUP_GRAPHS and HAVE_DOT tags are set to YES then doxygen -# will generate a graph for groups, showing the direct groups dependencies - -GROUP_GRAPHS = YES - -# If the UML_LOOK tag is set to YES doxygen will generate inheritance and -# collaboration diagrams in a style similar to the OMG's Unified Modeling -# Language. - -UML_LOOK = YES - -# If set to YES, the inheritance and collaboration graphs will show the -# relations between templates and their instances. - -TEMPLATE_RELATIONS = NO - -# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDE_GRAPH, and HAVE_DOT -# tags are set to YES then doxygen will generate a graph for each documented -# file showing the direct and indirect include dependencies of the file with -# other documented files. - -INCLUDE_GRAPH = YES - -# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDED_BY_GRAPH, and -# HAVE_DOT tags are set to YES then doxygen will generate a graph for each -# documented header file showing the documented files that directly or -# indirectly include this file. - -INCLUDED_BY_GRAPH = YES - -# If the CALL_GRAPH and HAVE_DOT options are set to YES then -# doxygen will generate a call dependency graph for every global function -# or class method. Note that enabling this option will significantly increase -# the time of a run. So in most cases it will be better to enable call graphs -# for selected functions only using the \callgraph command. - -CALL_GRAPH = NO - -# If the CALLER_GRAPH and HAVE_DOT tags are set to YES then -# doxygen will generate a caller dependency graph for every global function -# or class method. Note that enabling this option will significantly increase -# the time of a run. So in most cases it will be better to enable caller -# graphs for selected functions only using the \callergraph command. - -CALLER_GRAPH = NO - -# If the GRAPHICAL_HIERARCHY and HAVE_DOT tags are set to YES then doxygen -# will graphical hierarchy of all classes instead of a textual one. - -GRAPHICAL_HIERARCHY = YES - -# If the DIRECTORY_GRAPH, SHOW_DIRECTORIES and HAVE_DOT tags are set to YES -# then doxygen will show the dependencies a directory has on other directories -# in a graphical way. The dependency relations are determined by the #include -# relations between the files in the directories. - -DIRECTORY_GRAPH = YES - -# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images -# generated by dot. Possible values are png, jpg, or gif -# If left blank png will be used. - -DOT_IMAGE_FORMAT = png - -# The tag DOT_PATH can be used to specify the path where the dot tool can be -# found. If left blank, it is assumed the dot tool can be found in the path. - -DOT_PATH = - -# The DOTFILE_DIRS tag can be used to specify one or more directories that -# contain dot files that are included in the documentation (see the -# \dotfile command). - -DOTFILE_DIRS = - -# The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of -# nodes that will be shown in the graph. If the number of nodes in a graph -# becomes larger than this value, doxygen will truncate the graph, which is -# visualized by representing a node as a red box. Note that doxygen if the -# number of direct children of the root node in a graph is already larger than -# DOT_GRAPH_MAX_NODES then the graph will not be shown at all. Also note -# that the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH. - -DOT_GRAPH_MAX_NODES = 50 - -# The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the -# graphs generated by dot. A depth value of 3 means that only nodes reachable -# from the root by following a path via at most 3 edges will be shown. Nodes -# that lay further from the root node will be omitted. Note that setting this -# option to 1 or 2 may greatly reduce the computation time needed for large -# code bases. Also note that the size of a graph can be further restricted by -# DOT_GRAPH_MAX_NODES. Using a depth of 0 means no depth restriction. - -MAX_DOT_GRAPH_DEPTH = 0 - -# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent -# background. This is disabled by default, because dot on Windows does not -# seem to support this out of the box. Warning: Depending on the platform used, -# enabling this option may lead to badly anti-aliased labels on the edges of -# a graph (i.e. they become hard to read). - -DOT_TRANSPARENT = NO - -# Set the DOT_MULTI_TARGETS tag to YES allow dot to generate multiple output -# files in one run (i.e. multiple -o and -T options on the command line). This -# makes dot run faster, but since only newer versions of dot (>1.8.10) -# support this, this feature is disabled by default. - -DOT_MULTI_TARGETS = NO - -# If the GENERATE_LEGEND tag is set to YES (the default) Doxygen will -# generate a legend page explaining the meaning of the various boxes and -# arrows in the dot generated graphs. - -GENERATE_LEGEND = YES - -# If the DOT_CLEANUP tag is set to YES (the default) Doxygen will -# remove the intermediate dot files that are used to generate -# the various graphs. - -DOT_CLEANUP = YES - -#--------------------------------------------------------------------------- -# Options related to the search engine -#--------------------------------------------------------------------------- - -# The SEARCHENGINE tag specifies whether or not a search engine should be -# used. If set to NO the values of all tags below this one will be ignored. - -SEARCHENGINE = NO diff --git a/docs/Doxyfile.cmake b/docs/Doxyfile.cmake new file mode 100644 index 000000000..38ad84165 --- /dev/null +++ b/docs/Doxyfile.cmake @@ -0,0 +1,2354 @@ +# Doxyfile 1.8.7 + +# This file describes the settings to be used by the documentation system +# doxygen (www.doxygen.org) for a project. +# +# All text after a double hash (##) is considered a comment and is placed in +# front of the TAG it is preceding. +# +# All text after a single hash (#) is considered a comment and will be ignored. +# The format is: +# TAG = value [value, ...] +# For lists, items can also be appended using: +# TAG += value [value, ...] +# Values that contain spaces should be placed between quotes (\" \"). + +#--------------------------------------------------------------------------- +# Project related configuration options +#--------------------------------------------------------------------------- + +# This tag specifies the encoding used for all characters in the config file +# that follow. The default is UTF-8 which is also the encoding used for all text +# before the first occurrence of this tag. Doxygen uses libiconv (or the iconv +# built into libc) for the transcoding. See http://www.gnu.org/software/libiconv +# for the list of possible encodings. +# The default value is: UTF-8. + +DOXYFILE_ENCODING = UTF-8 + +# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by +# double-quotes, unless you are using Doxywizard) that should identify the +# project for which the documentation is generated. This name is used in the +# title of most generated pages and in a few other places. +# The default value is: My Project. + +PROJECT_NAME = OpenMW + +# The PROJECT_NUMBER tag can be used to enter a project or revision number. This +# could be handy for archiving the generated documentation or if some version +# control system is used. + +PROJECT_NUMBER = + +# Using the PROJECT_BRIEF tag one can provide an optional one line description +# for a project that appears at the top of each page and should give viewer a +# quick idea about the purpose of the project. Keep the description short. + +PROJECT_BRIEF = + +# With the PROJECT_LOGO tag one can specify an logo or icon that is included in +# the documentation. The maximum height of the logo should not exceed 55 pixels +# and the maximum width should not exceed 200 pixels. Doxygen will copy the logo +# to the output directory. + +PROJECT_LOGO = + +# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path +# into which the generated documentation will be written. If a relative path is +# entered, it will be relative to the location where doxygen was started. If +# left blank the current directory will be used. + +OUTPUT_DIRECTORY = @OpenMW_BINARY_DIR@/docs/Doxygen + +# If the CREATE_SUBDIRS tag is set to YES, then doxygen will create 4096 sub- +# directories (in 2 levels) under the output directory of each output format and +# will distribute the generated files over these directories. Enabling this +# option can be useful when feeding doxygen a huge amount of source files, where +# putting all generated files in the same directory would otherwise causes +# performance problems for the file system. +# The default value is: NO. + +CREATE_SUBDIRS = NO + +# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII +# characters to appear in the names of generated files. If set to NO, non-ASCII +# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode +# U+3044. +# The default value is: NO. + +ALLOW_UNICODE_NAMES = NO + +# The OUTPUT_LANGUAGE tag is used to specify the language in which all +# documentation generated by doxygen is written. Doxygen will use this +# information to generate all constant output in the proper language. +# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese, +# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States), +# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian, +# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages), +# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian, +# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian, +# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish, +# Ukrainian and Vietnamese. +# The default value is: English. + +OUTPUT_LANGUAGE = English + +# If the BRIEF_MEMBER_DESC tag is set to YES doxygen will include brief member +# descriptions after the members that are listed in the file and class +# documentation (similar to Javadoc). Set to NO to disable this. +# The default value is: YES. + +BRIEF_MEMBER_DESC = YES + +# If the REPEAT_BRIEF tag is set to YES doxygen will prepend the brief +# description of a member or function before the detailed description +# +# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the +# brief descriptions will be completely suppressed. +# The default value is: YES. + +REPEAT_BRIEF = YES + +# This tag implements a quasi-intelligent brief description abbreviator that is +# used to form the text in various listings. Each string in this list, if found +# as the leading text of the brief description, will be stripped from the text +# and the result, after processing the whole list, is used as the annotated +# text. Otherwise, the brief description is used as-is. If left blank, the +# following values are used ($name is automatically replaced with the name of +# the entity):The $name class, The $name widget, The $name file, is, provides, +# specifies, contains, represents, a, an and the. + +ABBREVIATE_BRIEF = "The $name class" \ + "The $name widget" \ + "The $name file" \ + is \ + provides \ + specifies \ + contains \ + represents \ + a \ + an \ + the + +# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then +# doxygen will generate a detailed section even if there is only a brief +# description. +# The default value is: NO. + +ALWAYS_DETAILED_SEC = NO + +# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all +# inherited members of a class in the documentation of that class as if those +# members were ordinary class members. Constructors, destructors and assignment +# operators of the base classes will not be shown. +# The default value is: NO. + +INLINE_INHERITED_MEMB = NO + +# If the FULL_PATH_NAMES tag is set to YES doxygen will prepend the full path +# before files name in the file list and in the header files. If set to NO the +# shortest path that makes the file name unique will be used +# The default value is: YES. + +FULL_PATH_NAMES = YES + +# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path. +# Stripping is only done if one of the specified strings matches the left-hand +# part of the path. The tag can be used to show relative paths in the file list. +# If left blank the directory from which doxygen is run is used as the path to +# strip. +# +# Note that you can specify absolute paths here, but also relative paths, which +# will be relative from the directory where doxygen is started. +# This tag requires that the tag FULL_PATH_NAMES is set to YES. + +STRIP_FROM_PATH = + +# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the +# path mentioned in the documentation of a class, which tells the reader which +# header file to include in order to use a class. If left blank only the name of +# the header file containing the class definition is used. Otherwise one should +# specify the list of include paths that are normally passed to the compiler +# using the -I flag. + +STRIP_FROM_INC_PATH = + +# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but +# less readable) file names. This can be useful is your file systems doesn't +# support long names like on DOS, Mac, or CD-ROM. +# The default value is: NO. + +SHORT_NAMES = NO + +# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the +# first line (until the first dot) of a Javadoc-style comment as the brief +# description. If set to NO, the Javadoc-style will behave just like regular Qt- +# style comments (thus requiring an explicit @brief command for a brief +# description.) +# The default value is: NO. + +JAVADOC_AUTOBRIEF = NO + +# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first +# line (until the first dot) of a Qt-style comment as the brief description. If +# set to NO, the Qt-style will behave just like regular Qt-style comments (thus +# requiring an explicit \brief command for a brief description.) +# The default value is: NO. + +QT_AUTOBRIEF = NO + +# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a +# multi-line C++ special comment block (i.e. a block of //! or /// comments) as +# a brief description. This used to be the default behavior. The new default is +# to treat a multi-line C++ comment block as a detailed description. Set this +# tag to YES if you prefer the old behavior instead. +# +# Note that setting this tag to YES also means that rational rose comments are +# not recognized any more. +# The default value is: NO. + +MULTILINE_CPP_IS_BRIEF = NO + +# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the +# documentation from any documented member that it re-implements. +# The default value is: YES. + +INHERIT_DOCS = YES + +# If the SEPARATE_MEMBER_PAGES tag is set to YES, then doxygen will produce a +# new page for each member. If set to NO, the documentation of a member will be +# part of the file/class/namespace that contains it. +# The default value is: NO. + +SEPARATE_MEMBER_PAGES = NO + +# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen +# uses this value to replace tabs by spaces in code fragments. +# Minimum value: 1, maximum value: 16, default value: 4. + +TAB_SIZE = 4 + +# This tag can be used to specify a number of aliases that act as commands in +# the documentation. An alias has the form: +# name=value +# For example adding +# "sideeffect=@par Side Effects:\n" +# will allow you to put the command \sideeffect (or @sideeffect) in the +# documentation, which will result in a user-defined paragraph with heading +# "Side Effects:". You can put \n's in the value part of an alias to insert +# newlines. + +ALIASES = + +# This tag can be used to specify a number of word-keyword mappings (TCL only). +# A mapping has the form "name=value". For example adding "class=itcl::class" +# will allow you to use the command class in the itcl::class meaning. + +TCL_SUBST = + +# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources +# only. Doxygen will then generate output that is more tailored for C. For +# instance, some of the names that are used will be different. The list of all +# members will be omitted, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_FOR_C = NO + +# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or +# Python sources only. Doxygen will then generate output that is more tailored +# for that language. For instance, namespaces will be presented as packages, +# qualified scopes will look different, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_JAVA = NO + +# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran +# sources. Doxygen will then generate output that is tailored for Fortran. +# The default value is: NO. + +OPTIMIZE_FOR_FORTRAN = NO + +# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL +# sources. Doxygen will then generate output that is tailored for VHDL. +# The default value is: NO. + +OPTIMIZE_OUTPUT_VHDL = NO + +# Doxygen selects the parser to use depending on the extension of the files it +# parses. With this tag you can assign which parser to use for a given +# extension. Doxygen has a built-in mapping, but you can override or extend it +# using this tag. The format is ext=language, where ext is a file extension, and +# language is one of the parsers supported by doxygen: IDL, Java, Javascript, +# C#, C, C++, D, PHP, Objective-C, Python, Fortran (fixed format Fortran: +# FortranFixed, free formatted Fortran: FortranFree, unknown formatted Fortran: +# Fortran. In the later case the parser tries to guess whether the code is fixed +# or free formatted code, this is the default for Fortran type files), VHDL. For +# instance to make doxygen treat .inc files as Fortran files (default is PHP), +# and .f files as C (default is Fortran), use: inc=Fortran f=C. +# +# Note For files without extension you can use no_extension as a placeholder. +# +# Note that for custom extensions you also need to set FILE_PATTERNS otherwise +# the files are not read by doxygen. + +EXTENSION_MAPPING = + +# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments +# according to the Markdown format, which allows for more readable +# documentation. See http://daringfireball.net/projects/markdown/ for details. +# The output of markdown processing is further processed by doxygen, so you can +# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in +# case of backward compatibilities issues. +# The default value is: YES. + +MARKDOWN_SUPPORT = YES + +# When enabled doxygen tries to link words that correspond to documented +# classes, or namespaces to their corresponding documentation. Such a link can +# be prevented in individual cases by by putting a % sign in front of the word +# or globally by setting AUTOLINK_SUPPORT to NO. +# The default value is: YES. + +AUTOLINK_SUPPORT = YES + +# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want +# to include (a tag file for) the STL sources as input, then you should set this +# tag to YES in order to let doxygen match functions declarations and +# definitions whose arguments contain STL classes (e.g. func(std::string); +# versus func(std::string) {}). This also make the inheritance and collaboration +# diagrams that involve STL classes more complete and accurate. +# The default value is: NO. + +BUILTIN_STL_SUPPORT = YES + +# If you use Microsoft's C++/CLI language, you should set this option to YES to +# enable parsing support. +# The default value is: NO. + +CPP_CLI_SUPPORT = NO + +# Set the SIP_SUPPORT tag to YES if your project consists of sip (see: +# http://www.riverbankcomputing.co.uk/software/sip/intro) sources only. Doxygen +# will parse them like normal C++ but will assume all classes use public instead +# of private inheritance when no explicit protection keyword is present. +# The default value is: NO. + +SIP_SUPPORT = NO + +# For Microsoft's IDL there are propget and propput attributes to indicate +# getter and setter methods for a property. Setting this option to YES will make +# doxygen to replace the get and set methods by a property in the documentation. +# This will only work if the methods are indeed getting or setting a simple +# type. If this is not the case, or you want to show the methods anyway, you +# should set this option to NO. +# The default value is: YES. + +IDL_PROPERTY_SUPPORT = YES + +# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC +# tag is set to YES, then doxygen will reuse the documentation of the first +# member in the group (if any) for the other members of the group. By default +# all members of a group must be documented explicitly. +# The default value is: NO. + +DISTRIBUTE_GROUP_DOC = NO + +# Set the SUBGROUPING tag to YES to allow class member groups of the same type +# (for instance a group of public functions) to be put as a subgroup of that +# type (e.g. under the Public Functions section). Set it to NO to prevent +# subgrouping. Alternatively, this can be done per class using the +# \nosubgrouping command. +# The default value is: YES. + +SUBGROUPING = YES + +# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions +# are shown inside the group in which they are included (e.g. using \ingroup) +# instead of on a separate page (for HTML and Man pages) or section (for LaTeX +# and RTF). +# +# Note that this feature does not work in combination with +# SEPARATE_MEMBER_PAGES. +# The default value is: NO. + +INLINE_GROUPED_CLASSES = NO + +# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions +# with only public data fields or simple typedef fields will be shown inline in +# the documentation of the scope in which they are defined (i.e. file, +# namespace, or group documentation), provided this scope is documented. If set +# to NO, structs, classes, and unions are shown on a separate page (for HTML and +# Man pages) or section (for LaTeX and RTF). +# The default value is: NO. + +INLINE_SIMPLE_STRUCTS = NO + +# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or +# enum is documented as struct, union, or enum with the name of the typedef. So +# typedef struct TypeS {} TypeT, will appear in the documentation as a struct +# with name TypeT. When disabled the typedef will appear as a member of a file, +# namespace, or class. And the struct will be named TypeS. This can typically be +# useful for C code in case the coding convention dictates that all compound +# types are typedef'ed and only the typedef is referenced, never the tag name. +# The default value is: NO. + +TYPEDEF_HIDES_STRUCT = NO + +# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This +# cache is used to resolve symbols given their name and scope. Since this can be +# an expensive process and often the same symbol appears multiple times in the +# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small +# doxygen will become slower. If the cache is too large, memory is wasted. The +# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range +# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536 +# symbols. At the end of a run doxygen will report the cache usage and suggest +# the optimal cache size from a speed point of view. +# Minimum value: 0, maximum value: 9, default value: 0. + +LOOKUP_CACHE_SIZE = 0 + +#--------------------------------------------------------------------------- +# Build related configuration options +#--------------------------------------------------------------------------- + +# If the EXTRACT_ALL tag is set to YES doxygen will assume all entities in +# documentation are documented, even if no documentation was available. Private +# class members and static file members will be hidden unless the +# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES. +# Note: This will also disable the warnings about undocumented members that are +# normally produced when WARNINGS is set to YES. +# The default value is: NO. + +EXTRACT_ALL = YES + +# If the EXTRACT_PRIVATE tag is set to YES all private members of a class will +# be included in the documentation. +# The default value is: NO. + +EXTRACT_PRIVATE = YES + +# If the EXTRACT_PACKAGE tag is set to YES all members with package or internal +# scope will be included in the documentation. +# The default value is: NO. + +EXTRACT_PACKAGE = NO + +# If the EXTRACT_STATIC tag is set to YES all static members of a file will be +# included in the documentation. +# The default value is: NO. + +EXTRACT_STATIC = YES + +# If the EXTRACT_LOCAL_CLASSES tag is set to YES classes (and structs) defined +# locally in source files will be included in the documentation. If set to NO +# only classes defined in header files are included. Does not have any effect +# for Java sources. +# The default value is: YES. + +EXTRACT_LOCAL_CLASSES = YES + +# This flag is only useful for Objective-C code. When set to YES local methods, +# which are defined in the implementation section but not in the interface are +# included in the documentation. If set to NO only methods in the interface are +# included. +# The default value is: NO. + +EXTRACT_LOCAL_METHODS = YES + +# If this flag is set to YES, the members of anonymous namespaces will be +# extracted and appear in the documentation as a namespace called +# 'anonymous_namespace{file}', where file will be replaced with the base name of +# the file that contains the anonymous namespace. By default anonymous namespace +# are hidden. +# The default value is: NO. + +EXTRACT_ANON_NSPACES = YES + +# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all +# undocumented members inside documented classes or files. If set to NO these +# members will be included in the various overviews, but no documentation +# section is generated. This option has no effect if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_MEMBERS = NO + +# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all +# undocumented classes that are normally visible in the class hierarchy. If set +# to NO these classes will be included in the various overviews. This option has +# no effect if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_CLASSES = NO + +# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend +# (class|struct|union) declarations. If set to NO these declarations will be +# included in the documentation. +# The default value is: NO. + +HIDE_FRIEND_COMPOUNDS = NO + +# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any +# documentation blocks found inside the body of a function. If set to NO these +# blocks will be appended to the function's detailed documentation block. +# The default value is: NO. + +HIDE_IN_BODY_DOCS = NO + +# The INTERNAL_DOCS tag determines if documentation that is typed after a +# \internal command is included. If the tag is set to NO then the documentation +# will be excluded. Set it to YES to include the internal documentation. +# The default value is: NO. + +INTERNAL_DOCS = NO + +# If the CASE_SENSE_NAMES tag is set to NO then doxygen will only generate file +# names in lower-case letters. If set to YES upper-case letters are also +# allowed. This is useful if you have classes or files whose names only differ +# in case and if your file system supports case sensitive file names. Windows +# and Mac users are advised to set this option to NO. +# The default value is: system dependent. + +CASE_SENSE_NAMES = YES + +# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with +# their full class and namespace scopes in the documentation. If set to YES the +# scope will be hidden. +# The default value is: NO. + +HIDE_SCOPE_NAMES = NO + +# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of +# the files that are included by a file in the documentation of that file. +# The default value is: YES. + +SHOW_INCLUDE_FILES = YES + +# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each +# grouped member an include statement to the documentation, telling the reader +# which file to include in order to use the member. +# The default value is: NO. + +SHOW_GROUPED_MEMB_INC = NO + +# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include +# files with double quotes in the documentation rather than with sharp brackets. +# The default value is: NO. + +FORCE_LOCAL_INCLUDES = NO + +# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the +# documentation for inline members. +# The default value is: YES. + +INLINE_INFO = YES + +# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the +# (detailed) documentation of file and class members alphabetically by member +# name. If set to NO the members will appear in declaration order. +# The default value is: YES. + +SORT_MEMBER_DOCS = YES + +# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief +# descriptions of file, namespace and class members alphabetically by member +# name. If set to NO the members will appear in declaration order. Note that +# this will also influence the order of the classes in the class list. +# The default value is: NO. + +SORT_BRIEF_DOCS = NO + +# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the +# (brief and detailed) documentation of class members so that constructors and +# destructors are listed first. If set to NO the constructors will appear in the +# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS. +# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief +# member documentation. +# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting +# detailed member documentation. +# The default value is: NO. + +SORT_MEMBERS_CTORS_1ST = NO + +# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy +# of group names into alphabetical order. If set to NO the group names will +# appear in their defined order. +# The default value is: NO. + +SORT_GROUP_NAMES = NO + +# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by +# fully-qualified names, including namespaces. If set to NO, the class list will +# be sorted only by class name, not including the namespace part. +# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. +# Note: This option applies only to the class list, not to the alphabetical +# list. +# The default value is: NO. + +SORT_BY_SCOPE_NAME = NO + +# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper +# type resolution of all parameters of a function it will reject a match between +# the prototype and the implementation of a member function even if there is +# only one candidate or it is obvious which candidate to choose by doing a +# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still +# accept a match between prototype and implementation in such cases. +# The default value is: NO. + +STRICT_PROTO_MATCHING = NO + +# The GENERATE_TODOLIST tag can be used to enable ( YES) or disable ( NO) the +# todo list. This list is created by putting \todo commands in the +# documentation. +# The default value is: YES. + +GENERATE_TODOLIST = YES + +# The GENERATE_TESTLIST tag can be used to enable ( YES) or disable ( NO) the +# test list. This list is created by putting \test commands in the +# documentation. +# The default value is: YES. + +GENERATE_TESTLIST = YES + +# The GENERATE_BUGLIST tag can be used to enable ( YES) or disable ( NO) the bug +# list. This list is created by putting \bug commands in the documentation. +# The default value is: YES. + +GENERATE_BUGLIST = YES + +# The GENERATE_DEPRECATEDLIST tag can be used to enable ( YES) or disable ( NO) +# the deprecated list. This list is created by putting \deprecated commands in +# the documentation. +# The default value is: YES. + +GENERATE_DEPRECATEDLIST= YES + +# The ENABLED_SECTIONS tag can be used to enable conditional documentation +# sections, marked by \if ... \endif and \cond +# ... \endcond blocks. + +ENABLED_SECTIONS = + +# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the +# initial value of a variable or macro / define can have for it to appear in the +# documentation. If the initializer consists of more lines than specified here +# it will be hidden. Use a value of 0 to hide initializers completely. The +# appearance of the value of individual variables and macros / defines can be +# controlled using \showinitializer or \hideinitializer command in the +# documentation regardless of this setting. +# Minimum value: 0, maximum value: 10000, default value: 30. + +MAX_INITIALIZER_LINES = 30 + +# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at +# the bottom of the documentation of classes and structs. If set to YES the list +# will mention the files that were used to generate the documentation. +# The default value is: YES. + +SHOW_USED_FILES = YES + +# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This +# will remove the Files entry from the Quick Index and from the Folder Tree View +# (if specified). +# The default value is: YES. + +SHOW_FILES = YES + +# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces +# page. This will remove the Namespaces entry from the Quick Index and from the +# Folder Tree View (if specified). +# The default value is: YES. + +SHOW_NAMESPACES = YES + +# The FILE_VERSION_FILTER tag can be used to specify a program or script that +# doxygen should invoke to get the current version for each file (typically from +# the version control system). Doxygen will invoke the program by executing (via +# popen()) the command command input-file, where command is the value of the +# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided +# by doxygen. Whatever the program writes to standard output is used as the file +# version. For an example see the documentation. + +FILE_VERSION_FILTER = + +# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed +# by doxygen. The layout file controls the global structure of the generated +# output files in an output format independent way. To create the layout file +# that represents doxygen's defaults, run doxygen with the -l option. You can +# optionally specify a file name after the option, if omitted DoxygenLayout.xml +# will be used as the name of the layout file. +# +# Note that if you run doxygen from a directory containing a file called +# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE +# tag is left empty. + +LAYOUT_FILE = + +# The CITE_BIB_FILES tag can be used to specify one or more bib files containing +# the reference definitions. This must be a list of .bib files. The .bib +# extension is automatically appended if omitted. This requires the bibtex tool +# to be installed. See also http://en.wikipedia.org/wiki/BibTeX for more info. +# For LaTeX the style of the bibliography can be controlled using +# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the +# search path. Do not use file names with spaces, bibtex cannot handle them. See +# also \cite for info how to create references. + +CITE_BIB_FILES = + +#--------------------------------------------------------------------------- +# Configuration options related to warning and progress messages +#--------------------------------------------------------------------------- + +# The QUIET tag can be used to turn on/off the messages that are generated to +# standard output by doxygen. If QUIET is set to YES this implies that the +# messages are off. +# The default value is: NO. + +QUIET = NO + +# The WARNINGS tag can be used to turn on/off the warning messages that are +# generated to standard error ( stderr) by doxygen. If WARNINGS is set to YES +# this implies that the warnings are on. +# +# Tip: Turn warnings on while writing the documentation. +# The default value is: YES. + +WARNINGS = YES + +# If the WARN_IF_UNDOCUMENTED tag is set to YES, then doxygen will generate +# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag +# will automatically be disabled. +# The default value is: YES. + +WARN_IF_UNDOCUMENTED = NO + +# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for +# potential errors in the documentation, such as not documenting some parameters +# in a documented function, or documenting parameters that don't exist or using +# markup commands wrongly. +# The default value is: YES. + +WARN_IF_DOC_ERROR = YES + +# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that +# are documented, but have no documentation for their parameters or return +# value. If set to NO doxygen will only warn about wrong or incomplete parameter +# documentation, but not about the absence of documentation. +# The default value is: NO. + +WARN_NO_PARAMDOC = NO + +# The WARN_FORMAT tag determines the format of the warning messages that doxygen +# can produce. The string should contain the $file, $line, and $text tags, which +# will be replaced by the file and line number from which the warning originated +# and the warning text. Optionally the format may contain $version, which will +# be replaced by the version of the file (if it could be obtained via +# FILE_VERSION_FILTER) +# The default value is: $file:$line: $text. + +WARN_FORMAT = "$file:$line: $text" + +# The WARN_LOGFILE tag can be used to specify a file to which warning and error +# messages should be written. If left blank the output is written to standard +# error (stderr). + +WARN_LOGFILE = + +#--------------------------------------------------------------------------- +# Configuration options related to the input files +#--------------------------------------------------------------------------- + +# The INPUT tag is used to specify the files and/or directories that contain +# documented source files. You may enter file names like myfile.cpp or +# directories like /usr/src/myproject. Separate the files or directories with +# spaces. +# Note: If this tag is empty the current directory is searched. + +INPUT = @OpenMW_SOURCE_DIR@/apps \ + @OpenMW_SOURCE_DIR@/components \ + @OpenMW_SOURCE_DIR@/libs \ + @OpenMW_BINARY_DIR@/docs/mainpage.hpp + +# This tag can be used to specify the character encoding of the source files +# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses +# libiconv (or the iconv built into libc) for the transcoding. See the libiconv +# documentation (see: http://www.gnu.org/software/libiconv) for the list of +# possible encodings. +# The default value is: UTF-8. + +INPUT_ENCODING = UTF-8 + +# If the value of the INPUT tag contains directories, you can use the +# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and +# *.h) to filter out the source-files in the directories. If left blank the +# following patterns are tested:*.c, *.cc, *.cxx, *.cpp, *.c++, *.java, *.ii, +# *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h, *.hh, *.hxx, *.hpp, +# *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc, *.m, *.markdown, +# *.md, *.mm, *.dox, *.py, *.f90, *.f, *.for, *.tcl, *.vhd, *.vhdl, *.ucf, +# *.qsf, *.as and *.js. + +FILE_PATTERNS = *.c \ + *.cc \ + *.cxx \ + *.cpp \ + *.c++ \ + *.java \ + *.ii \ + *.ixx \ + *.ipp \ + *.i++ \ + *.inl \ + *.h \ + *.hh \ + *.hxx \ + *.hpp \ + *.h++ \ + *.idl \ + *.odl \ + *.cs \ + *.php \ + *.php3 \ + *.inc \ + *.m \ + *.mm \ + *.dox \ + *.py \ + *.f90 \ + *.f \ + *.vhd \ + *.vhdl + +# The RECURSIVE tag can be used to specify whether or not subdirectories should +# be searched for input files as well. +# The default value is: NO. + +RECURSIVE = YES + +# The EXCLUDE tag can be used to specify files and/or directories that should be +# excluded from the INPUT source files. This way you can easily exclude a +# subdirectory from a directory tree whose root is specified with the INPUT tag. +# +# Note that relative paths are relative to the directory from which doxygen is +# run. + +EXCLUDE = + +# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or +# directories that are symbolic links (a Unix file system feature) are excluded +# from the input. +# The default value is: NO. + +EXCLUDE_SYMLINKS = NO + +# If the value of the INPUT tag contains directories, you can use the +# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude +# certain files from those directories. +# +# Note that the wildcards are matched against the file with absolute path, so to +# exclude all test directories for example use the pattern */test/* + +EXCLUDE_PATTERNS = + +# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names +# (namespaces, classes, functions, etc.) that should be excluded from the +# output. The symbol name can be a fully qualified name, a word, or if the +# wildcard * is used, a substring. Examples: ANamespace, AClass, +# AClass::ANamespace, ANamespace::*Test +# +# Note that the wildcards are matched against the file with absolute path, so to +# exclude all test directories use the pattern */test/* + +EXCLUDE_SYMBOLS = + +# The EXAMPLE_PATH tag can be used to specify one or more files or directories +# that contain example code fragments that are included (see the \include +# command). + +EXAMPLE_PATH = + +# If the value of the EXAMPLE_PATH tag contains directories, you can use the +# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and +# *.h) to filter out the source-files in the directories. If left blank all +# files are included. + +EXAMPLE_PATTERNS = * + +# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be +# searched for input files to be used with the \include or \dontinclude commands +# irrespective of the value of the RECURSIVE tag. +# The default value is: NO. + +EXAMPLE_RECURSIVE = NO + +# The IMAGE_PATH tag can be used to specify one or more files or directories +# that contain images that are to be included in the documentation (see the +# \image command). + +IMAGE_PATH = + +# The INPUT_FILTER tag can be used to specify a program that doxygen should +# invoke to filter for each input file. Doxygen will invoke the filter program +# by executing (via popen()) the command: +# +# +# +# where is the value of the INPUT_FILTER tag, and is the +# name of an input file. Doxygen will then use the output that the filter +# program writes to standard output. If FILTER_PATTERNS is specified, this tag +# will be ignored. +# +# Note that the filter must not add or remove lines; it is applied before the +# code is scanned, but not when the output code is generated. If lines are added +# or removed, the anchors will not be placed correctly. + +INPUT_FILTER = + +# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern +# basis. Doxygen will compare the file name with each pattern and apply the +# filter if there is a match. The filters are a list of the form: pattern=filter +# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how +# filters are used. If the FILTER_PATTERNS tag is empty or if none of the +# patterns match the file name, INPUT_FILTER is applied. + +FILTER_PATTERNS = + +# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using +# INPUT_FILTER ) will also be used to filter the input files that are used for +# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES). +# The default value is: NO. + +FILTER_SOURCE_FILES = NO + +# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file +# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and +# it is also possible to disable source filtering for a specific pattern using +# *.ext= (so without naming a filter). +# This tag requires that the tag FILTER_SOURCE_FILES is set to YES. + +FILTER_SOURCE_PATTERNS = + +# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that +# is part of the input, its contents will be placed on the main page +# (index.html). This can be useful if you have a project on for instance GitHub +# and want to reuse the introduction page also for the doxygen output. + +USE_MDFILE_AS_MAINPAGE = + +#--------------------------------------------------------------------------- +# Configuration options related to source browsing +#--------------------------------------------------------------------------- + +# If the SOURCE_BROWSER tag is set to YES then a list of source files will be +# generated. Documented entities will be cross-referenced with these sources. +# +# Note: To get rid of all source code in the generated output, make sure that +# also VERBATIM_HEADERS is set to NO. +# The default value is: NO. + +SOURCE_BROWSER = NO + +# Setting the INLINE_SOURCES tag to YES will include the body of functions, +# classes and enums directly into the documentation. +# The default value is: NO. + +INLINE_SOURCES = NO + +# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any +# special comment blocks from generated source code fragments. Normal C, C++ and +# Fortran comments will always remain visible. +# The default value is: YES. + +STRIP_CODE_COMMENTS = YES + +# If the REFERENCED_BY_RELATION tag is set to YES then for each documented +# function all documented functions referencing it will be listed. +# The default value is: NO. + +REFERENCED_BY_RELATION = NO + +# If the REFERENCES_RELATION tag is set to YES then for each documented function +# all documented entities called/used by that function will be listed. +# The default value is: NO. + +REFERENCES_RELATION = NO + +# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set +# to YES, then the hyperlinks from functions in REFERENCES_RELATION and +# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will +# link to the documentation. +# The default value is: YES. + +REFERENCES_LINK_SOURCE = YES + +# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the +# source code will show a tooltip with additional information such as prototype, +# brief description and links to the definition and documentation. Since this +# will make the HTML file larger and loading of large files a bit slower, you +# can opt to disable this feature. +# The default value is: YES. +# This tag requires that the tag SOURCE_BROWSER is set to YES. + +SOURCE_TOOLTIPS = YES + +# If the USE_HTAGS tag is set to YES then the references to source code will +# point to the HTML generated by the htags(1) tool instead of doxygen built-in +# source browser. The htags tool is part of GNU's global source tagging system +# (see http://www.gnu.org/software/global/global.html). You will need version +# 4.8.6 or higher. +# +# To use it do the following: +# - Install the latest version of global +# - Enable SOURCE_BROWSER and USE_HTAGS in the config file +# - Make sure the INPUT points to the root of the source tree +# - Run doxygen as normal +# +# Doxygen will invoke htags (and that will in turn invoke gtags), so these +# tools must be available from the command line (i.e. in the search path). +# +# The result: instead of the source browser generated by doxygen, the links to +# source code will now point to the output of htags. +# The default value is: NO. +# This tag requires that the tag SOURCE_BROWSER is set to YES. + +USE_HTAGS = NO + +# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a +# verbatim copy of the header file for each class for which an include is +# specified. Set to NO to disable this. +# See also: Section \class. +# The default value is: YES. + +VERBATIM_HEADERS = YES + +#--------------------------------------------------------------------------- +# Configuration options related to the alphabetical class index +#--------------------------------------------------------------------------- + +# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all +# compounds will be generated. Enable this if the project contains a lot of +# classes, structs, unions or interfaces. +# The default value is: YES. + +ALPHABETICAL_INDEX = NO + +# The COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns in +# which the alphabetical index list will be split. +# Minimum value: 1, maximum value: 20, default value: 5. +# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. + +COLS_IN_ALPHA_INDEX = 5 + +# In case all classes in a project start with a common prefix, all classes will +# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag +# can be used to specify a prefix (or a list of prefixes) that should be ignored +# while generating the index headers. +# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. + +IGNORE_PREFIX = + +#--------------------------------------------------------------------------- +# Configuration options related to the HTML output +#--------------------------------------------------------------------------- + +# If the GENERATE_HTML tag is set to YES doxygen will generate HTML output +# The default value is: YES. + +GENERATE_HTML = YES + +# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a +# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of +# it. +# The default directory is: html. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_OUTPUT = html + +# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each +# generated HTML page (for example: .htm, .php, .asp). +# The default value is: .html. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FILE_EXTENSION = .html + +# The HTML_HEADER tag can be used to specify a user-defined HTML header file for +# each generated HTML page. If the tag is left blank doxygen will generate a +# standard header. +# +# To get valid HTML the header file that includes any scripts and style sheets +# that doxygen needs, which is dependent on the configuration options used (e.g. +# the setting GENERATE_TREEVIEW). It is highly recommended to start with a +# default header using +# doxygen -w html new_header.html new_footer.html new_stylesheet.css +# YourConfigFile +# and then modify the file new_header.html. See also section "Doxygen usage" +# for information on how to generate the default header that doxygen normally +# uses. +# Note: The header is subject to change so you typically have to regenerate the +# default header when upgrading to a newer version of doxygen. For a description +# of the possible markers and block names see the documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_HEADER = + +# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each +# generated HTML page. If the tag is left blank doxygen will generate a standard +# footer. See HTML_HEADER for more information on how to generate a default +# footer and what special commands can be used inside the footer. See also +# section "Doxygen usage" for information on how to generate the default footer +# that doxygen normally uses. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FOOTER = + +# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style +# sheet that is used by each HTML page. It can be used to fine-tune the look of +# the HTML output. If left blank doxygen will generate a default style sheet. +# See also section "Doxygen usage" for information on how to generate the style +# sheet that doxygen normally uses. +# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as +# it is more robust and this tag (HTML_STYLESHEET) will in the future become +# obsolete. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_STYLESHEET = + +# The HTML_EXTRA_STYLESHEET tag can be used to specify an additional user- +# defined cascading style sheet that is included after the standard style sheets +# created by doxygen. Using this option one can overrule certain style aspects. +# This is preferred over using HTML_STYLESHEET since it does not replace the +# standard style sheet and is therefor more robust against future updates. +# Doxygen will copy the style sheet file to the output directory. For an example +# see the documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_EXTRA_STYLESHEET = + +# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or +# other source files which should be copied to the HTML output directory. Note +# that these files will be copied to the base HTML output directory. Use the +# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these +# files. In the HTML_STYLESHEET file, use the file name only. Also note that the +# files will be copied as-is; there are no commands or markers available. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_EXTRA_FILES = + +# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen +# will adjust the colors in the stylesheet and background images according to +# this color. Hue is specified as an angle on a colorwheel, see +# http://en.wikipedia.org/wiki/Hue for more information. For instance the value +# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300 +# purple, and 360 is red again. +# Minimum value: 0, maximum value: 359, default value: 220. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_HUE = 220 + +# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors +# in the HTML output. For a value of 0 the output will use grayscales only. A +# value of 255 will produce the most vivid colors. +# Minimum value: 0, maximum value: 255, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_SAT = 100 + +# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the +# luminance component of the colors in the HTML output. Values below 100 +# gradually make the output lighter, whereas values above 100 make the output +# darker. The value divided by 100 is the actual gamma applied, so 80 represents +# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not +# change the gamma. +# Minimum value: 40, maximum value: 240, default value: 80. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_GAMMA = 80 + +# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML +# page will contain the date and time when the page was generated. Setting this +# to NO can help when comparing the output of multiple runs. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_TIMESTAMP = YES + +# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML +# documentation will contain sections that can be hidden and shown after the +# page has loaded. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_DYNAMIC_SECTIONS = NO + +# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries +# shown in the various tree structured indices initially; the user can expand +# and collapse entries dynamically later on. Doxygen will expand the tree to +# such a level that at most the specified number of entries are visible (unless +# a fully collapsed tree already exceeds this amount). So setting the number of +# entries 1 will produce a full collapsed tree by default. 0 is a special value +# representing an infinite number of entries and will result in a full expanded +# tree by default. +# Minimum value: 0, maximum value: 9999, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_INDEX_NUM_ENTRIES = 100 + +# If the GENERATE_DOCSET tag is set to YES, additional index files will be +# generated that can be used as input for Apple's Xcode 3 integrated development +# environment (see: http://developer.apple.com/tools/xcode/), introduced with +# OSX 10.5 (Leopard). To create a documentation set, doxygen will generate a +# Makefile in the HTML output directory. Running make will produce the docset in +# that directory and running make install will install the docset in +# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at +# startup. See http://developer.apple.com/tools/creatingdocsetswithdoxygen.html +# for more information. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_DOCSET = NO + +# This tag determines the name of the docset feed. A documentation feed provides +# an umbrella under which multiple documentation sets from a single provider +# (such as a company or product suite) can be grouped. +# The default value is: Doxygen generated docs. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_FEEDNAME = "OpenMW Documentation" + +# This tag specifies a string that should uniquely identify the documentation +# set bundle. This should be a reverse domain-name style string, e.g. +# com.mycompany.MyDocSet. Doxygen will append .docset to the name. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_BUNDLE_ID = org.openmw + +# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify +# the documentation publisher. This should be a reverse domain-name style +# string, e.g. com.mycompany.MyDocSet.documentation. +# The default value is: org.doxygen.Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_PUBLISHER_ID = org.openmw + +# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher. +# The default value is: Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_PUBLISHER_NAME = OpenMW + +# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three +# additional HTML index files: index.hhp, index.hhc, and index.hhk. The +# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop +# (see: http://www.microsoft.com/en-us/download/details.aspx?id=21138) on +# Windows. +# +# The HTML Help Workshop contains a compiler that can convert all HTML output +# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML +# files are now used as the Windows 98 help format, and will replace the old +# Windows help format (.hlp) on all Windows platforms in the future. Compressed +# HTML files also contain an index, a table of contents, and you can search for +# words in the documentation. The HTML workshop also contains a viewer for +# compressed HTML files. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_HTMLHELP = NO + +# The CHM_FILE tag can be used to specify the file name of the resulting .chm +# file. You can add a path in front of the file if the result should not be +# written to the html output directory. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +CHM_FILE = + +# The HHC_LOCATION tag can be used to specify the location (absolute path +# including file name) of the HTML help compiler ( hhc.exe). If non-empty +# doxygen will try to run the HTML help compiler on the generated index.hhp. +# The file has to be specified with full path. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +HHC_LOCATION = + +# The GENERATE_CHI flag controls if a separate .chi index file is generated ( +# YES) or that it should be included in the master .chm file ( NO). +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +GENERATE_CHI = NO + +# The CHM_INDEX_ENCODING is used to encode HtmlHelp index ( hhk), content ( hhc) +# and project file content. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +CHM_INDEX_ENCODING = + +# The BINARY_TOC flag controls whether a binary table of contents is generated ( +# YES) or a normal table of contents ( NO) in the .chm file. Furthermore it +# enables the Previous and Next buttons. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +BINARY_TOC = NO + +# The TOC_EXPAND flag can be set to YES to add extra items for group members to +# the table of contents of the HTML help documentation and to the tree view. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +TOC_EXPAND = NO + +# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and +# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that +# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help +# (.qch) of the generated HTML documentation. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_QHP = NO + +# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify +# the file name of the resulting .qch file. The path specified is relative to +# the HTML output folder. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QCH_FILE = + +# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help +# Project output. For more information please see Qt Help Project / Namespace +# (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#namespace). +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_NAMESPACE = org.openmw + +# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt +# Help Project output. For more information please see Qt Help Project / Virtual +# Folders (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#virtual- +# folders). +# The default value is: doc. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_VIRTUAL_FOLDER = doc + +# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom +# filter to add. For more information please see Qt Help Project / Custom +# Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom- +# filters). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_CUST_FILTER_NAME = + +# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the +# custom filter to add. For more information please see Qt Help Project / Custom +# Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom- +# filters). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_CUST_FILTER_ATTRS = + +# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this +# project's filter section matches. Qt Help Project / Filter Attributes (see: +# http://qt-project.org/doc/qt-4.8/qthelpproject.html#filter-attributes). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_SECT_FILTER_ATTRS = + +# The QHG_LOCATION tag can be used to specify the location of Qt's +# qhelpgenerator. If non-empty doxygen will try to run qhelpgenerator on the +# generated .qhp file. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHG_LOCATION = + +# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be +# generated, together with the HTML files, they form an Eclipse help plugin. To +# install this plugin and make it available under the help contents menu in +# Eclipse, the contents of the directory containing the HTML and XML files needs +# to be copied into the plugins directory of eclipse. The name of the directory +# within the plugins directory should be the same as the ECLIPSE_DOC_ID value. +# After copying Eclipse needs to be restarted before the help appears. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_ECLIPSEHELP = NO + +# A unique identifier for the Eclipse help plugin. When installing the plugin +# the directory name containing the HTML and XML files should also have this +# name. Each documentation set should have its own identifier. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES. + +ECLIPSE_DOC_ID = org.openmw + +# If you want full control over the layout of the generated HTML pages it might +# be necessary to disable the index and replace it with your own. The +# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top +# of each HTML page. A value of NO enables the index and the value YES disables +# it. Since the tabs in the index contain the same information as the navigation +# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +DISABLE_INDEX = NO + +# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index +# structure should be generated to display hierarchical information. If the tag +# value is set to YES, a side panel will be generated containing a tree-like +# index structure (just like the one that is generated for HTML Help). For this +# to work a browser that supports JavaScript, DHTML, CSS and frames is required +# (i.e. any modern browser). Windows users are probably better off using the +# HTML help feature. Via custom stylesheets (see HTML_EXTRA_STYLESHEET) one can +# further fine-tune the look of the index. As an example, the default style +# sheet generated by doxygen has an example that shows how to put an image at +# the root of the tree instead of the PROJECT_NAME. Since the tree basically has +# the same information as the tab index, you could consider setting +# DISABLE_INDEX to YES when enabling this option. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_TREEVIEW = NO + +# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that +# doxygen will group on one line in the generated HTML documentation. +# +# Note that a value of 0 will completely suppress the enum values from appearing +# in the overview section. +# Minimum value: 0, maximum value: 20, default value: 4. +# This tag requires that the tag GENERATE_HTML is set to YES. + +ENUM_VALUES_PER_LINE = 4 + +# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used +# to set the initial width (in pixels) of the frame in which the tree is shown. +# Minimum value: 0, maximum value: 1500, default value: 250. +# This tag requires that the tag GENERATE_HTML is set to YES. + +TREEVIEW_WIDTH = 250 + +# When the EXT_LINKS_IN_WINDOW option is set to YES doxygen will open links to +# external symbols imported via tag files in a separate window. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +EXT_LINKS_IN_WINDOW = NO + +# Use this tag to change the font size of LaTeX formulas included as images in +# the HTML documentation. When you change the font size after a successful +# doxygen run you need to manually remove any form_*.png images from the HTML +# output directory to force them to be regenerated. +# Minimum value: 8, maximum value: 50, default value: 10. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FORMULA_FONTSIZE = 10 + +# Use the FORMULA_TRANPARENT tag to determine whether or not the images +# generated for formulas are transparent PNGs. Transparent PNGs are not +# supported properly for IE 6.0, but are supported on all modern browsers. +# +# Note that when changing this option you need to delete any form_*.png files in +# the HTML output directory before the changes have effect. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FORMULA_TRANSPARENT = YES + +# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see +# http://www.mathjax.org) which uses client side Javascript for the rendering +# instead of using prerendered bitmaps. Use this if you do not have LaTeX +# installed or if you want to formulas look prettier in the HTML output. When +# enabled you may also need to install MathJax separately and configure the path +# to it using the MATHJAX_RELPATH option. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +USE_MATHJAX = YES + +# When MathJax is enabled you can set the default output format to be used for +# the MathJax output. See the MathJax site (see: +# http://docs.mathjax.org/en/latest/output.html) for more details. +# Possible values are: HTML-CSS (which is slower, but has the best +# compatibility), NativeMML (i.e. MathML) and SVG. +# The default value is: HTML-CSS. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_FORMAT = HTML-CSS + +# When MathJax is enabled you need to specify the location relative to the HTML +# output directory using the MATHJAX_RELPATH option. The destination directory +# should contain the MathJax.js script. For instance, if the mathjax directory +# is located at the same level as the HTML output directory, then +# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax +# Content Delivery Network so you can quickly see the result without installing +# MathJax. However, it is strongly recommended to install a local copy of +# MathJax from http://www.mathjax.org before deployment. +# The default value is: http://cdn.mathjax.org/mathjax/latest. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_RELPATH = http://cdn.mathjax.org/mathjax/latest + +# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax +# extension names that should be enabled during MathJax rendering. For example +# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_EXTENSIONS = + +# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces +# of code that will be used on startup of the MathJax code. See the MathJax site +# (see: http://docs.mathjax.org/en/latest/output.html) for more details. For an +# example see the documentation. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_CODEFILE = + +# When the SEARCHENGINE tag is enabled doxygen will generate a search box for +# the HTML output. The underlying search engine uses javascript and DHTML and +# should work on any modern browser. Note that when using HTML help +# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET) +# there is already a search function so this one should typically be disabled. +# For large projects the javascript based search engine can be slow, then +# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to +# search using the keyboard; to jump to the search box use + S +# (what the is depends on the OS and browser, but it is typically +# , /